NIP-72: 審核社群
Nostr 審核社群定義,支援版主管理和貼文審核機制
概述
NIP-72 定義了在 Nostr 上建立審核社群(Moderated Communities)的標準。 與 NIP-29 的中繼器群組不同,NIP-72 社群的審核是在客戶端層面進行, 允許任何人發布貼文,但只有經過版主批准的內容才會被客戶端顯示。
事件類型
| Kind | 名稱 | 說明 |
|---|---|---|
34550 | 社群定義 | 定義社群資訊、版主和中繼器 |
1111 | 社群貼文 | 發布到社群的文字內容(NIP-22) |
4550 | 審核批准 | 版主對貼文的批准事件 |
社群定義 (Kind 34550)
社群定義是一個可替換事件,包含社群的基本資訊、版主列表和中繼器設定。
{
"kind": 34550,
"tags": [
["d", "nostr-dev"],
["name", "Nostr 開發者社群"],
["description", "討論 Nostr 協議開發的社群"],
["image", "https://example.com/community-logo.png"],
["p", "<moderator-pubkey-1>", "wss://relay.example.com", "moderator"],
["p", "<moderator-pubkey-2>", "wss://relay.example.com", "moderator"],
["relay", "wss://community-relay.example.com", "author"],
["relay", "wss://community-relay.example.com", "requests"],
["relay", "wss://community-relay.example.com", "approvals"]
],
"content": ""
} 社群標籤
| 標籤 | 說明 | 必要性 |
|---|---|---|
d | 社群唯一識別符 | 必要 |
name | 社群顯示名稱(優先於 d 標籤) | 建議 |
description | 社群描述 | 建議 |
image | 社群圖片 URL | 選填 |
p | 版主公鑰(帶 "moderator" 角色) | 建議 |
relay | 指定用途的中繼器 | 選填 |
中繼器角色
author:社群創建者發布定義的中繼器requests:用戶提交貼文請求的中繼器approvals:版主發布批准事件的中繼器
社群貼文 (Kind 1111)
使用 NIP-22 格式的 kind 1111 事件發布社群貼文。貼文必須包含 a 標籤指向目標社群。
{
"kind": 1111,
"content": "這是我在社群發布的第一篇貼文!",
"tags": [
["a", "34550:<community-creator-pubkey>:nostr-dev", "wss://relay.example.com"],
["K", "34550"],
["k", "1111"]
]
} 向後相容:舊版使用 kind 1 事件發布社群貼文,但新實作應使用 kind 1111。
審核批准 (Kind 4550)
版主透過發布 kind 4550 事件來批准貼文。任何人都可以發布批准事件表達對貼文的認可, 但客戶端通常只會顯示由社群定義中指定的版主所批准的內容。
{
"kind": 4550,
"content": "{\"id\":\"...\",\"pubkey\":\"...\",\"content\":\"原始貼文內容...\",\"kind\":1111,...}",
"tags": [
["a", "34550:<community-creator-pubkey>:nostr-dev", "wss://relay.example.com"],
["e", "<post-event-id>", "wss://relay.example.com"],
["p", "<post-author-pubkey>"],
["k", "1111"]
]
} 批准事件標籤
| 標籤 | 說明 |
|---|---|
a | 指向社群定義(格式:34550:<pubkey>:<d-tag>) |
e | 被批准的貼文事件 ID |
p | 貼文作者的公鑰(用於通知) |
k | 原始貼文的 kind |
content 欄位
批准事件的 content 必須包含被批准貼文的完整 JSON 字串化內容。 這允許客戶端在無法取得原始貼文時仍能顯示內容。
審核策略
對於可替換事件(如長文 kind 30023),版主可以選擇不同的批准策略:
1. 逐版本批准 (Per-version)
只使用 e 標籤。每次作者更新內容都需要重新批准。
{
"tags": [
["a", "34550:<community-pubkey>:community-id"],
["e", "<specific-version-id>"],
["p", "<author-pubkey>"],
["k", "30023"]
]
} 2. 全面授權 (Blanket authorization)
使用 a 標籤指向貼文。作者可以更新內容而無需額外批准。
{
"tags": [
["a", "34550:<community-pubkey>:community-id"],
["a", "30023:<author-pubkey>:article-id"],
["p", "<author-pubkey>"],
["k", "30023"]
]
} 3. 雙重標記
同時使用 e 和 a 標籤,顯示原始批准版本和最新版本。
跨社群轉發
社群支援使用 NIP-18 轉發(kind 6 或 kind 16)進行跨社群分享:
{
"kind": 6,
"content": "{\"id\":\"...\",\"content\":\"原始貼文...\",\"kind\":1,...}",
"tags": [
["a", "34550:<target-community-pubkey>:target-community"],
["e", "<original-post-id>"],
["p", "<original-author-pubkey>"]
]
} TypeScript 實作
建立社群
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools';
interface CommunityConfig {
identifier: string;
name: string;
description?: string;
image?: string;
moderators: Array<{ pubkey: string; relay?: string }>;
relays?: {
author?: string;
requests?: string;
approvals?: string;
};
rules?: string[];
}
function createCommunity(config: CommunityConfig, secretKey: Uint8Array) {
const tags: string[][] = [
['d', config.identifier],
['name', config.name],
];
if (config.description) {
tags.push(['description', config.description]);
}
if (config.image) {
tags.push(['image', config.image]);
}
// 添加版主
config.moderators.forEach((mod) => {
const pTag = ['p', mod.pubkey];
if (mod.relay) pTag.push(mod.relay);
pTag.push('moderator');
tags.push(pTag);
});
// 添加中繼器設定
if (config.relays) {
if (config.relays.author) {
tags.push(['relay', config.relays.author, 'author']);
}
if (config.relays.requests) {
tags.push(['relay', config.relays.requests, 'requests']);
}
if (config.relays.approvals) {
tags.push(['relay', config.relays.approvals, 'approvals']);
}
}
// 添加規則
if (config.rules) {
config.rules.forEach((rule, index) => {
tags.push(['rule', rule, (index + 1).toString()]);
});
}
const event = {
kind: 34550,
content: '',
tags,
created_at: Math.floor(Date.now() / 1000),
};
return finalizeEvent(event, secretKey);
}
// 使用範例
const sk = generateSecretKey();
const pk = getPublicKey(sk);
const community = createCommunity(
{
identifier: 'nostr-taiwan',
name: 'Nostr 台灣社群',
description: '台灣 Nostr 愛好者的討論空間',
image: 'https://example.com/tw-nostr.png',
moderators: [
{ pubkey: pk, relay: 'wss://relay.damus.io' },
],
relays: {
author: 'wss://relay.damus.io',
requests: 'wss://relay.damus.io',
approvals: 'wss://relay.damus.io',
},
rules: [
'保持友善和尊重',
'禁止垃圾訊息和廣告',
'討論需與 Nostr 相關',
],
},
sk
);
console.log(community); 發布社群貼文
interface CommunityReference {
creatorPubkey: string;
identifier: string;
relay?: string;
}
function createCommunityPost(
content: string,
community: CommunityReference,
secretKey: Uint8Array,
options?: {
replyTo?: { eventId: string; authorPubkey: string };
hashtags?: string[];
}
) {
const communityTag = `34550:${community.creatorPubkey}:${community.identifier}`;
const tags: string[][] = [
['a', communityTag, community.relay || ''],
['K', '34550'],
['k', '1111'],
];
// 添加回覆標籤
if (options?.replyTo) {
tags.push(['e', options.replyTo.eventId, '', 'reply']);
tags.push(['p', options.replyTo.authorPubkey]);
}
// 添加主題標籤
if (options?.hashtags) {
options.hashtags.forEach((tag) => {
tags.push(['t', tag]);
});
}
const event = {
kind: 1111,
content,
tags,
created_at: Math.floor(Date.now() / 1000),
};
return finalizeEvent(event, secretKey);
}
// 使用範例
const post = createCommunityPost(
'大家好!這是我在社群的第一篇貼文。',
{
creatorPubkey: '<community-creator-pubkey>',
identifier: 'nostr-taiwan',
relay: 'wss://relay.damus.io',
},
sk,
{
hashtags: ['nostr', '台灣'],
}
); 批准貼文
function approvePost(
post: any,
community: CommunityReference,
moderatorSecretKey: Uint8Array,
options?: {
blanketAuthorization?: boolean;
}
) {
const communityTag = `34550:${community.creatorPubkey}:${community.identifier}`;
const tags: string[][] = [
['a', communityTag, community.relay || ''],
['p', post.pubkey],
['k', post.kind.toString()],
];
// 添加貼文引用
if (options?.blanketAuthorization && post.kind >= 30000 && post.kind < 40000) {
// 可替換事件的全面授權
const dTag = post.tags.find((t: string[]) => t[0] === 'd')?.[1];
if (dTag) {
tags.push(['a', `${post.kind}:${post.pubkey}:${dTag}`]);
}
}
// 總是添加特定版本引用
tags.push(['e', post.id, community.relay || '']);
const event = {
kind: 4550,
content: JSON.stringify(post),
tags,
created_at: Math.floor(Date.now() / 1000),
};
return finalizeEvent(event, moderatorSecretKey);
}
// 使用範例
const approval = approvePost(
post,
{
creatorPubkey: '<community-creator-pubkey>',
identifier: 'nostr-taiwan',
relay: 'wss://relay.damus.io',
},
sk
); 查詢社群內容
import { SimplePool } from 'nostr-tools';
const pool = new SimplePool();
const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
// 查詢社群定義
async function getCommunity(creatorPubkey: string, identifier: string) {
const events = await pool.querySync(relays, {
kinds: [34550],
authors: [creatorPubkey],
'#d': [identifier],
});
return events[0];
}
// 查詢社群貼文(待審核)
async function getPendingPosts(communityTag: string) {
const posts = await pool.querySync(relays, {
kinds: [1111],
'#a': [communityTag],
});
return posts;
}
// 查詢已批准的貼文
async function getApprovedPosts(
communityTag: string,
moderatorPubkeys: string[]
) {
const approvals = await pool.querySync(relays, {
kinds: [4550],
authors: moderatorPubkeys,
'#a': [communityTag],
});
// 從批准事件中提取貼文
const posts = approvals.map((approval) => {
try {
return JSON.parse(approval.content);
} catch {
return null;
}
}).filter(Boolean);
return posts;
}
// 使用範例
const communityTag = '34550:<creator-pubkey>:nostr-taiwan';
const community = await getCommunity('<creator-pubkey>', 'nostr-taiwan');
if (community) {
const moderators = community.tags
.filter((t: string[]) => t[0] === 'p' && t[3] === 'moderator')
.map((t: string[]) => t[1]);
const approvedPosts = await getApprovedPosts(communityTag, moderators);
console.log(`已批准貼文: ${approvedPosts.length}`);
} 審核流程
- 用戶發布貼文:使用 kind 1111 發布到社群
- 版主審核:版主查看待審核貼文
- 發布批准:版主發布 kind 4550 批准事件
- 客戶端顯示:客戶端只顯示已批准的貼文
注意:審核是在客戶端層面進行的,中繼器不會過濾未批准的貼文。 這意味著所有貼文都會被儲存,只是客戶端選擇只顯示已批准的內容。
使用場景
主題討論區
- 技術討論社群
- 興趣愛好群組
- 地區社群
內容策展
- 精選文章集合
- 新聞聚合
- 教學資源
組織管理
- 公司公告板
- 專案協作空間
- 會員專區
最佳實踐
- 設定多位版主:避免單點故障,確保審核效率
- 明確社群規則:在描述中說明什麼內容會被批准
- 及時審核:定期處理待審核貼文,保持社群活躍
- 保存原始內容:批准事件應包含完整的原始貼文
- 考慮用戶體驗:讓用戶知道貼文正在等待審核
與 NIP-29 比較
| 特性 | NIP-72 審核社群 | NIP-29 中繼器群組 |
|---|---|---|
| 審核層級 | 客戶端 | 中繼器 |
| 貼文儲存 | 所有貼文都儲存 | 可拒絕未授權貼文 |
| 隱私性 | 較低(所有貼文可見) | 較高(可限制存取) |
| 去中心化 | 高(不依賴特定中繼器) | 中(依賴群組中繼器) |
相關 NIPs
參考資源
已複製連結