跳至主要內容
入門

NIP-09 事件刪除

深入了解 Nostr 的事件刪除機制,刪除請求和中繼器處理。

8 分鐘

什麼是 NIP-09?

NIP-09 定義了 Nostr 中的事件刪除機制。用戶可以發布 kind 5 事件來請求刪除 自己先前發布的事件。這是一種「刪除請求」而非強制刪除,中繼器可以選擇是否執行。

重要: 刪除在 Nostr 中不是強制性的。中繼器可能忽略刪除請求, 其他用戶可能已經保存了你的事件。發布前請謹慎考慮。

事件結構

{
  "kind": 5,
  "pubkey": "<author-pubkey>",
  "content": "刪除原因(可選)",
  "tags": [
    ["e", "<event-id-to-delete>"],
    ["e", "<another-event-id>"],
    ["a", "<kind>:<pubkey>:<d-tag>"]  // 刪除可替換事件
  ],
  "created_at": 1234567890,
  "id": "...",
  "sig": "..."
}

刪除類型

標籤 用途 格式
"e" 刪除特定事件 ["e", "<event-id>"]
"a" 刪除可替換事件 ["a", "<kind>:<pubkey>:<d>"]

範例

刪除單個事件

{
  "kind": 5,
  "content": "發錯了",
  "tags": [
    ["e", "abc123..."]
  ]
}

刪除多個事件

{
  "kind": 5,
  "content": "清理舊貼文",
  "tags": [
    ["e", "abc123..."],
    ["e", "def456..."],
    ["e", "ghi789..."]
  ]
}

刪除可替換事件(如長文)

{
  "kind": 5,
  "content": "撤回文章",
  "tags": [
    ["a", "30023:abc123...:my-article-slug"]
  ]
}

// "a" 標籤格式:<kind>:<pubkey>:<d-tag>
// 用於 kind 30000+ 的可替換事件

程式碼範例

發送刪除請求

async function deleteEvent(eventId, reason = '') {
  const deleteEvent = {
    kind: 5,
    created_at: Math.floor(Date.now() / 1000),
    content: reason,
    tags: [
      ['e', eventId]
    ]
  }

  // 簽名
  const signed = await window.nostr.signEvent(deleteEvent)

  // 發送到中繼器
  relay.send(JSON.stringify(['EVENT', signed]))

  return signed
}

// 使用
await deleteEvent('abc123...', '發布錯誤')
await deleteEvent('def456...')  // 無原因

批量刪除

async function deleteEvents(eventIds, reason = '') {
  const deleteEvent = {
    kind: 5,
    created_at: Math.floor(Date.now() / 1000),
    content: reason,
    tags: eventIds.map(id => ['e', id])
  }

  const signed = await window.nostr.signEvent(deleteEvent)
  relay.send(JSON.stringify(['EVENT', signed]))

  return signed
}

// 批量刪除
await deleteEvents([
  'abc123...',
  'def456...',
  'ghi789...'
], '清理舊內容')

檢查事件是否已刪除

async function isDeleted(eventId, authorPubkey) {
  return new Promise((resolve) => {
    const sub = relay.subscribe([{
      kinds: [5],
      authors: [authorPubkey],
      '#e': [eventId]
    }])

    let deleted = false

    sub.on('event', (event) => {
      // 確認刪除事件是同一作者發的
      if (event.pubkey === authorPubkey) {
        deleted = true
      }
    })

    sub.on('eose', () => {
      sub.close()
      resolve(deleted)
    })
  })
}

// 使用
const deleted = await isDeleted('abc123...', 'author-pubkey')
if (deleted) {
  console.log('此事件已被作者刪除')
}

中繼器行為

刪除事件

中繼器收到 kind 5 後,刪除對應的事件

推薦

拒絕重發

已刪除的事件不再接受重新發布

推薦

保留刪除記錄

保存 kind 5 事件供客戶端查詢

可選

忽略刪除

某些中繼器可能完全忽略刪除請求

允許

驗證規則

function validateDeletion(deleteEvent, targetEvent) {
  // 1. 必須是 kind 5
  if (deleteEvent.kind !== 5) return false

  // 2. 刪除者必須是原事件作者
  if (deleteEvent.pubkey !== targetEvent.pubkey) return false

  // 3. 刪除事件必須引用目標事件
  const eTags = deleteEvent.tags.filter(t => t[0] === 'e')
  const hasTarget = eTags.some(t => t[1] === targetEvent.id)
  if (!hasTarget) return false

  return true
}

// 注意:只有作者可以刪除自己的事件
// 其他人的刪除請求應該被忽略

客戶端處理

應該做

  • • 查詢時檢查 kind 5 事件
  • • 隱藏已刪除的事件
  • • 顯示「已刪除」提示
  • • 允許用戶刪除自己的內容

避免

  • • 執行他人的刪除請求
  • • 假設刪除一定成功
  • • 永久刪除本地快取
  • • 忽略刪除事件

查詢刪除事件

// 查詢特定事件是否被刪除
["REQ", "check-deleted", {
  "kinds": [5],
  "#e": ["<event-id>"]
}]

// 查詢用戶的所有刪除請求
["REQ", "user-deletions", {
  "kinds": [5],
  "authors": ["<pubkey>"]
}]

// 查詢特定可替換事件的刪除
["REQ", "check-addr-deleted", {
  "kinds": [5],
  "#a": ["30023:<pubkey>:<d-tag>"]
}]

限制與考量

刪除不是萬能的:

  • • 其他用戶可能已保存你的事件
  • • 某些中繼器可能不支援刪除
  • • 刪除前的內容可能已被引用或轉發
  • • 網路快照服務可能保留記錄

最佳實踐: 在發布敏感內容前三思。使用刪除功能作為「盡力而為」的補救措施, 而非可靠的隱私保護機制。

已複製連結
已複製到剪貼簿