入門
NIP-56 檢舉
Nostr 內容檢舉標準,讓用戶可以標記不當內容供客戶端和中繼器參考。
8 分鐘
概述
NIP-56 定義了內容檢舉標準,使用 kind 1984 事件讓用戶可以標記不當內容。 這些檢舉可以被客戶端和中繼器用於內容審核決策,但最終的處理方式 由各平台自行決定。
去中心化審核
Nostr 沒有中央審核機構。檢舉是一種信號,客戶端可以根據用戶的社交圖譜 (如朋友的檢舉)來決定如何處理內容。
事件結構
檢舉使用 kind 1984 事件:
{`{
"kind": 1984,
"content": "檢舉原因說明(選填)",
"tags": [
["p", "<被檢舉者公鑰>", "<檢舉類型>"],
["e", "<被檢舉事件ID>", "<檢舉類型>"]
],
...
}`} Kind 1984
這個 kind 號碼是對喬治·歐威爾小說《1984》的引用, 暗示了審核系統的雙面性。
檢舉類型
| 類型 | 說明 |
|---|---|
| nudity | 裸露或成人內容 |
| malware | 病毒、木馬、間諜軟體等 |
| profanity | 髒話或仇恨言論 |
| illegal | 違法內容(依司法管轄區) |
| spam | 垃圾訊息 |
| impersonation | 冒充他人 |
| other | 其他 |
檢舉用戶
檢舉用戶的整體行為:
{`{
"kind": 1984,
"content": "此帳號持續發送垃圾訊息",
"tags": [
["p", "<被檢舉者公鑰>", "spam"]
],
...
}`} 檢舉特定事件
檢舉特定貼文或事件,需要同時包含 e 和 p 標籤:
{`{
"kind": 1984,
"content": "這則貼文包含不當圖片",
"tags": [
["e", "<事件ID>", "nudity"],
["p", "<事件作者公鑰>"]
],
...
}`} 檢舉媒體
檢舉特定的媒體檔案(使用檔案雜湊):
{`{
"kind": 1984,
"content": "此圖片包含惡意軟體",
"tags": [
["x", "<檔案SHA-256雜湊>"],
["e", "<包含此媒體的事件ID>"],
["p", "<發布者公鑰>"],
["server", "https://media.example.com"]
],
...
}`} 實作
建立檢舉
{`import { finalizeEvent } from 'nostr-tools';
type ReportType =
| 'nudity'
| 'malware'
| 'profanity'
| 'illegal'
| 'spam'
| 'impersonation'
| 'other';
// 檢舉用戶
function reportUser(
privateKey: Uint8Array,
userPubkey: string,
reportType: ReportType,
reason?: string
) {
return finalizeEvent({
kind: 1984,
created_at: Math.floor(Date.now() / 1000),
tags: [
['p', userPubkey, reportType],
],
content: reason || '',
}, privateKey);
}
// 檢舉事件
function reportEvent(
privateKey: Uint8Array,
eventId: string,
authorPubkey: string,
reportType: ReportType,
reason?: string
) {
return finalizeEvent({
kind: 1984,
created_at: Math.floor(Date.now() / 1000),
tags: [
['e', eventId, reportType],
['p', authorPubkey],
],
content: reason || '',
}, privateKey);
}
// 檢舉媒體
function reportMedia(
privateKey: Uint8Array,
fileHash: string,
eventId: string,
authorPubkey: string,
reportType: ReportType,
serverUrl?: string,
reason?: string
) {
const tags: string[][] = [
['x', fileHash],
['e', eventId],
['p', authorPubkey],
];
if (serverUrl) {
tags.push(['server', serverUrl]);
}
return finalizeEvent({
kind: 1984,
created_at: Math.floor(Date.now() / 1000),
tags,
content: reason || '',
}, privateKey);
}
// 使用範例
const spamReport = reportUser(
privateKey,
'abc123...',
'spam',
'此帳號每小時發送數十則廣告'
);
const nudityReport = reportEvent(
privateKey,
'def456...',
'ghi789...',
'nudity',
'貼文包含未標記的成人內容'
);`} 查詢檢舉
{`import { SimplePool } from 'nostr-tools';
const pool = new SimplePool();
const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
// 查詢針對特定用戶的檢舉
async function getReportsForUser(userPubkey: string) {
return await pool.querySync(relays, {
kinds: [1984],
'#p': [userPubkey],
});
}
// 查詢針對特定事件的檢舉
async function getReportsForEvent(eventId: string) {
return await pool.querySync(relays, {
kinds: [1984],
'#e': [eventId],
});
}
// 查詢來自特定用戶的檢舉(檢舉者)
async function getReportsByUser(reporterPubkey: string) {
return await pool.querySync(relays, {
kinds: [1984],
authors: [reporterPubkey],
});
}`} 分析檢舉
{`interface ReportSummary {
pubkey: string;
totalReports: number;
byType: Record;
reporters: string[];
}
// 分析用戶的檢舉統計
function analyzeReports(reports: Event[]): ReportSummary {
const summary: ReportSummary = {
pubkey: '',
totalReports: reports.length,
byType: {
nudity: 0,
malware: 0,
profanity: 0,
illegal: 0,
spam: 0,
impersonation: 0,
other: 0,
},
reporters: [],
};
for (const report of reports) {
// 取得被檢舉者
const pTag = report.tags.find(t => t[0] === 'p');
if (pTag) {
summary.pubkey = pTag[1];
const reportType = (pTag[2] || 'other') as ReportType;
summary.byType[reportType]++;
}
// 取得事件檢舉類型
const eTag = report.tags.find(t => t[0] === 'e');
if (eTag && eTag[2]) {
const reportType = eTag[2] as ReportType;
summary.byType[reportType]++;
}
// 記錄檢舉者
if (!summary.reporters.includes(report.pubkey)) {
summary.reporters.push(report.pubkey);
}
}
return summary;
}`} 客戶端使用
客戶端可以根據社交圖譜來使用檢舉資訊:
{`// 檢查內容是否被朋友檢舉
async function checkFriendReports(
eventId: string,
myFollowing: string[]
): Promise<{
reported: boolean;
friendReports: Event[];
}> {
const reports = await getReportsForEvent(eventId);
// 過濾出朋友的檢舉
const friendReports = reports.filter(r =>
myFollowing.includes(r.pubkey)
);
return {
reported: friendReports.length > 0,
friendReports,
};
}
// 根據檢舉決定顯示方式
function getContentDisplay(
event: Event,
friendReports: Event[]
): 'show' | 'blur' | 'hide' {
if (friendReports.length === 0) {
return 'show';
}
// 多個朋友檢舉 → 隱藏
if (friendReports.length >= 3) {
return 'hide';
}
// 單一朋友檢舉 → 模糊
return 'blur';
}`} 中繼器使用
注意:自動審核風險
中繼器不應該自動根據檢舉進行審核,因為這容易被濫用(惡意檢舉)。 建議只根據受信任的審核者的檢舉來採取行動。
{`// 中繼器端:只信任特定審核者的檢舉
const trustedModerators = [
'npub1moderator1...',
'npub1moderator2...',
];
function shouldBlockContent(
reports: Event[],
trustedModerators: string[]
): boolean {
// 只考慮受信任審核者的檢舉
const trustedReports = reports.filter(r =>
trustedModerators.includes(r.pubkey)
);
return trustedReports.length > 0;
}`} React 組件範例
{`interface ReportButtonProps {
eventId?: string;
userPubkey: string;
onReport: (type: ReportType, reason: string) => void;
}
function ReportButton({ eventId, userPubkey, onReport }: ReportButtonProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedType, setSelectedType] = useState('other');
const [reason, setReason] = useState('');
const reportTypes: { value: ReportType; label: string }[] = [
{ value: 'spam', label: '垃圾訊息' },
{ value: 'nudity', label: '成人內容' },
{ value: 'profanity', label: '仇恨言論' },
{ value: 'impersonation', label: '冒充他人' },
{ value: 'malware', label: '惡意軟體' },
{ value: 'illegal', label: '違法內容' },
{ value: 'other', label: '其他' },
];
const handleSubmit = () => {
onReport(selectedType, reason);
setIsOpen(false);
setReason('');
};
return (
<>
{isOpen && (
檢舉{eventId ? '貼文' : '用戶'}
)}
>
);
}`} 最佳實踐
- 社交過濾:優先考慮朋友的檢舉,而非陌生人
- 閾值設定:設定合理的檢舉數量閾值再採取行動
- 透明度:讓用戶知道內容為何被隱藏或模糊
- 申訴機制:提供被檢舉者申訴的管道
- 避免自動化:不要完全自動化審核決策
注意事項
- 濫用風險:檢舉系統可能被用於騷擾或審查
- 主觀性:不同用戶對「不當」的定義不同
- 無法刪除:檢舉無法真正刪除內容,只是信號
- 隱私:檢舉是公開的,檢舉者身份可見
相關 NIP
參考資源
已複製連結