Skip to content

Commit

Permalink
add DeletionEvent (nostr-sdk#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
bryanmontz authored and RandyMcMillan committed Sep 1, 2024
1 parent 377796e commit 0491fea
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 0 deletions.
20 changes: 20 additions & 0 deletions Sources/NostrSDK/EventCreating.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,26 @@ public extension EventCreating {
return try DirectMessageEvent(content: encryptedMessage, tags: [recipientTag], signedBy: keypair)
}

/// Creates a ``DeletionEvent`` (kind 5) and signs it with the provided ``Keypair``.
/// - Parameters:
/// - events: The events the signer would like to request deletion for.
/// - keypair: The Keypair to sign with.
/// - Returns: The signed ``DeletionEvent``.
///
/// > Important: Events can only be deleted using the same keypair that was used to create them.
/// See [NIP-09 Specification](https://github.com/nostr-protocol/nips/blob/master/09.md)
func delete(events: [NostrEvent], reason: String? = nil, signedBy keypair: Keypair) throws -> DeletionEvent {
// Verify that the events being deleted were created with the same keypair.
let creatorValidatedEvents = events.filter { $0.pubkey == keypair.publicKey.hex }

guard !creatorValidatedEvents.isEmpty else {
throw EventCreatingError.invalidInput
}

let tags = creatorValidatedEvents.map { Tag(name: .event, value: $0.id) }
return try DeletionEvent(content: reason ?? "", tags: tags, signedBy: keypair)
}

/// Creates a ``TextNoteRepostEvent`` (kind 6) or ``GenericRepostEvent`` (kind 16) based on the kind of the event being reposted and signs it with the provided ``Keypair``.
/// - Parameters:
/// - event: The event to repost.
Expand Down
10 changes: 10 additions & 0 deletions Sources/NostrSDK/EventKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable {
/// See [NIP-04 - Direct Messages](https://github.com/nostr-protocol/nips/blob/master/04.md)
case directMessage

/// This kind of event indicates that the author requests that the events in the included
/// tags should be deleted.
/// > Note: This event can only *request* that the listed events be deleted. In reality, they
/// may not be deleted by all clients or relays.
///
/// See [NIP-09 - Event Deletion](https://github.com/nostr-protocol/nips/blob/master/09.md)
case deletion

/// This kind of note is used to signal to followers that another event is worth reading.
///
/// > Note: The reposted event must be a kind 1 text note.
Expand Down Expand Up @@ -70,6 +78,7 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable {
.recommendServer,
.contactList,
.directMessage,
.deletion,
.repost,
.reaction,
.genericRepost,
Expand All @@ -88,6 +97,7 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable {
case .recommendServer: return 2
case .contactList: return 3
case .directMessage: return 4
case .deletion: return 5
case .repost: return 6
case .reaction: return 7
case .genericRepost: return 16
Expand Down
38 changes: 38 additions & 0 deletions Sources/NostrSDK/Events/DeletionEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// DeletionEvent.swift
//
//
// Created by Bryan Montz on 10/29/23.
//

import Foundation

/// An event that contains one or more references to other events that the
/// event creator would like to delete.
///
/// > Note: [NIP-09 Specification](https://github.com/nostr-protocol/nips/blob/master/09.md)
public final class DeletionEvent: NostrEvent {

public required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}

@available(*, unavailable, message: "This initializer is unavailable for this class.")
override init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws {
try super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, signedBy: keypair)
}

init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws {
try super.init(kind: .deletion, content: content, tags: tags, createdAt: createdAt, signedBy: keypair)
}

/// The reason the creator of the event gave for deleting the included events.
public var reason: String {
content
}

/// The event ids that the creator requests deletion for.
public var deletedEventIds: [String] {
tags.filter { $0.name == .event }.map { $0.value }
}
}
21 changes: 21 additions & 0 deletions Tests/NostrSDKTests/EventCreatingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,28 @@ final class EventCreatingTests: XCTestCase, EventCreating, EventVerifying, Fixtu

try verifyEvent(event)
}

func testDeletionEvent() throws {
let noteToDelete: TextNoteEvent = try decodeFixture(filename: "text_note_deletable")
let reason = "Didn't mean to post"

let event = try delete(events: [noteToDelete], reason: reason, signedBy: Keypair.test)

XCTAssertEqual(event.kind, .deletion)

XCTAssertEqual(event.reason, "Didn't mean to post")
XCTAssertEqual(event.deletedEventIds, ["fa5ed84fc8eeb959fd39ad8e48388cfc33075991ef8e50064cfcecfd918bb91b"])

try verifyEvent(event)
}

func testDeletionEventFailsWithMismatchedKey() throws {
let noteToDelete: TextNoteEvent = try decodeFixture(filename: "text_note")
let reason = "Didn't mean to post"

XCTAssertThrowsError(try delete(events: [noteToDelete], reason: reason, signedBy: Keypair.test))
}

func testRepostTextNoteEvent() throws {
let noteToRepost: TextNoteEvent = try decodeFixture(filename: "text_note")

Expand Down
18 changes: 18 additions & 0 deletions Tests/NostrSDKTests/Fixtures/text_note_deletable.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"id": "fa5ed84fc8eeb959fd39ad8e48388cfc33075991ef8e50064cfcecfd918bb91b",
"pubkey": "9947f9659dd80c3682402b612f5447e28249997fb3709500c32a585eb0977340",
"created_at": 1682080184,
"kind": 1,
"tags": [
[
"e",
"93930d65435d49db723499335473920795e7f13c45600dcfad922135cf44bd63"
],
[
"p",
"f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9"
]
],
"content": "I think it stays persistent on your profile, but interface setting doesn’t persist. Bug. ",
"sig": "96e6667348b2b1fc5f6e73e68fb1605f571ad044077dda62a35c15eb8290f2c4559935db461f8466df3dcf39bc2e11984c5344f65aabee4520dd6653d74cdc09"
}

0 comments on commit 0491fea

Please sign in to comment.