Skip to content

Commit

Permalink
Refactor out common non-NIP-01 tags out of NostrEvent file into separ…
Browse files Browse the repository at this point in the history
…ate protocols for better code separation (#197)
  • Loading branch information
tyiu authored Nov 3, 2024
1 parent 43978e1 commit 7dcc12d
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 109 deletions.
4 changes: 2 additions & 2 deletions Sources/NostrSDK/Events/LabelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ public extension LabelEvent {
/// Otherwise, the label is attached to this event itself as the target.
///
/// See [NIP-32 Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md).
public protocol LabelBuilding: NostrEventBuilding {}
public extension LabelBuilding {
public protocol LabelTagBuilding: NostrEventBuilding {}
public extension LabelTagBuilding {
/// Labels an event in a given namespace.
///
/// Namespaces can be any string but SHOULD be unambiguous by using a well-defined namespace (such as an ISO standard) or reverse domain name notation.
Expand Down
48 changes: 2 additions & 46 deletions Sources/NostrSDK/Events/NostrEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
/// A structure that describes a Nostr event.
///
/// > Note: [NIP-01 Specification](https://github.com/nostr-protocol/nips/blob/master/01.md#events-and-signatures)
public class NostrEvent: Codable, Equatable, Hashable, ContentWarningTagInterpreting, LabelTagInterpreting {
public class NostrEvent: Codable, Equatable, Hashable, AlternativeSummaryTagInterpreting, ContentWarningTagInterpreting, ExpirationTagInterpreting, LabelTagInterpreting {
public static func == (lhs: NostrEvent, rhs: NostrEvent) -> Bool {
lhs.id == rhs.id &&
lhs.pubkey == rhs.pubkey &&
Expand Down Expand Up @@ -150,33 +150,6 @@ public class NostrEvent: Codable, Equatable, Hashable, ContentWarningTagInterpre
tags.compactMap { EventCoordinates(eventCoordinatesTag: $0) }
}

/// A short human-readable plaintext summary of what the event is about
/// when the event kind is part of a custom protocol and isn't meant to be read as text (like kind:1).
/// See [NIP-31 - Dealing with unknown event kinds](https://github.com/nostr-protocol/nips/blob/master/31.md).
public var alternativeSummary: String? {
firstValueForTagName(.alternativeSummary)
}

/// Unix timestamp at which the message SHOULD be considered expired (by relays and clients) and SHOULD be deleted by relays.
/// See [NIP-40 - Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md).
public var expiration: Int64? {
if let expiration = firstValueForTagName(.expiration) {
return Int64(expiration)
} else {
return nil
}
}

/// Whether the message SHOULD be considered expired (by relays and clients) and SHOULD be deleted by relays.
/// See [NIP-40 - Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md).
public var isExpired: Bool {
if let expiration {
return Int64(Date.now.timeIntervalSince1970) >= expiration
} else {
return false
}
}

/// All tags with the provided name.
public func allTags(withTagName tagName: TagName) -> [Tag] {
tags.filter { $0.name == tagName.rawValue }
Expand Down Expand Up @@ -318,7 +291,7 @@ public protocol NostrEventBuilding {

public extension NostrEvent {
/// Builder of a ``NostrEvent`` of type `T`.
class Builder<T: NostrEvent>: NostrEventBuilding, ContentWarningTagBuilding, LabelBuilding {
class Builder<T: NostrEvent>: NostrEventBuilding, AlternativeSummaryTagBuilding, ContentWarningTagBuilding, ExpirationTagBuilding, LabelTagBuilding {
public typealias EventType = T

/// The event kind.
Expand Down Expand Up @@ -382,23 +355,6 @@ public extension NostrEvent {
return self
}

/// Specifies a short human-readable plaintext summary of what the event is about
/// when the event kind is part of a custom protocol and isn't meant to be read as text (like kind:1).
/// See [NIP-31 - Dealing with unknown event kinds](https://github.com/nostr-protocol/nips/blob/master/31.md).
@discardableResult
public final func alternativeSummary(_ alternativeSummary: String) -> Self {
tags.append(Tag(name: .alternativeSummary, value: alternativeSummary))
return self
}

/// Specifies a unix timestamp at which the message SHOULD be considered expired (by relays and clients) and SHOULD be deleted by relays.
/// See [NIP-40 - Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md).
@discardableResult
public final func expiration(_ expiration: Int64) -> Self {
tags.append(Tag(name: .expiration, value: String(expiration)))
return self
}

public func build(signedBy keypair: Keypair) throws -> T {
try T(
kind: kind,
Expand Down
33 changes: 33 additions & 0 deletions Sources/NostrSDK/Events/Tags/AlternativeSummaryTag.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// AlternativeSummaryTag.swift
// NostrSDK
//
// Created by Terry Yiu on 11/3/24.
//

import Foundation

/// Interprets the "alt" alternative summary tag.
///
/// See [NIP-31 - Dealing with unknown event kinds](https://github.com/nostr-protocol/nips/blob/master/31.md).
public protocol AlternativeSummaryTagInterpreting: NostrEvent {}
public extension AlternativeSummaryTagInterpreting {
/// A short human-readable plaintext summary of what the event is about
/// when the event kind is part of a custom protocol and isn't meant to be read as text (like kind:1).
var alternativeSummary: String? {
firstValueForTagName(.alternativeSummary)
}
}

/// Builder that adds an "alt" alternative summary tag to an event.
///
/// See [NIP-31 - Dealing with unknown event kinds](https://github.com/nostr-protocol/nips/blob/master/31.md).
public protocol AlternativeSummaryTagBuilding: NostrEventBuilding {}
public extension AlternativeSummaryTagBuilding {
/// Specifies a short human-readable plaintext summary of what the event is about
/// when the event kind is part of a custom protocol and isn't meant to be read as text (like kind:1).
@discardableResult
func alternativeSummary(_ alternativeSummary: String) -> Self {
appendTags(Tag(name: .alternativeSummary, value: alternativeSummary))
}
}
44 changes: 44 additions & 0 deletions Sources/NostrSDK/Events/Tags/ExpirationTag.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// ExpirationTag.swift
// NostrSDK
//
// Created by Terry Yiu on 11/3/24.
//

import Foundation

/// Interprets the expiration tag.
///
/// See [NIP-40 - Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md).
public protocol ExpirationTagInterpreting: NostrEvent {}
public extension ExpirationTagInterpreting {
/// Unix timestamp at which the message SHOULD be considered expired (by relays and clients) and SHOULD be deleted by relays.
var expiration: Int64? {
if let expiration = firstValueForTagName(.expiration) {
return Int64(expiration)
} else {
return nil
}
}

/// Whether the message SHOULD be considered expired (by relays and clients) and SHOULD be deleted by relays.
var isExpired: Bool {
if let expiration {
return Int64(Date.now.timeIntervalSince1970) >= expiration
} else {
return false
}
}
}

/// Builder that adds an expiration to an event.
///
/// See [NIP-40 - Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md).
public protocol ExpirationTagBuilding: NostrEventBuilding {}
public extension ExpirationTagBuilding {
/// Specifies a unix timestamp at which the message SHOULD be considered expired (by relays and clients) and SHOULD be deleted by relays.
@discardableResult
func expiration(_ expiration: Int64) -> Self {
appendTags(Tag(name: .expiration, value: String(expiration)))
}
}
61 changes: 1 addition & 60 deletions Tests/NostrSDKTests/Events/NostrEventTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
@testable import NostrSDK
import XCTest

final class NostrEventTests: XCTestCase, EventVerifying, FixtureLoading, MetadataCoding {
final class NostrEventTests: XCTestCase, FixtureLoading, MetadataCoding {

func testEquatable() throws {
let textNoteEvent: TextNoteEvent = try decodeFixture(filename: "text_note")
Expand Down Expand Up @@ -68,63 +68,4 @@ final class NostrEventTests: XCTestCase, EventVerifying, FixtureLoading, Metadat
XCTAssertEqual(metadata.relays?[1], relay2)
}

func testAlternativeSummary() throws {
let alternativeSummary = "Alternative summary to display for clients that do not support this event kind."
let customEvent = try NostrEvent.Builder(kind: EventKind(rawValue: 23456))
.alternativeSummary(alternativeSummary)
.build(signedBy: .test)
XCTAssertEqual(customEvent.alternativeSummary, alternativeSummary)

let decodedCustomEventWithAltTag: NostrEvent = try decodeFixture(filename: "custom_event_alt_tag")
XCTAssertEqual(decodedCustomEventWithAltTag.alternativeSummary, alternativeSummary)
}

func testLabels() throws {
let event = try TextNoteEvent.Builder()
.appendLabels("IT-MI", "US-CA", namespace: "ISO-3166-2")
.appendLabels("en", namespace: "ISO-639-1")
.appendLabels("Milan", "San Francisco", mark: "cities")
.appendLabels("Italy", "United States of America")
.content("It's beautiful here in Milan and wonderful there in San Francisco!")
.build(signedBy: .test)

XCTAssertEqual(event.labels(for: "ISO-3166-2"), ["IT-MI", "US-CA"])
XCTAssertEqual(event.labels(for: "ISO-639-1"), ["en"])
XCTAssertEqual(event.labels(for: "cities"), ["Milan", "San Francisco"])
XCTAssertEqual(event.labels(for: nil), ["Italy", "United States of America"])
XCTAssertEqual(event.labels(for: "ugc"), ["Italy", "United States of America"])
XCTAssertEqual(event.labels(for: "doesnotexist"), [])

XCTAssertEqual(event.labelNamespaces, ["ISO-3166-2", "ISO-639-1"])

let labels = event.labels
XCTAssertEqual(labels["ISO-3166-2"], ["IT-MI", "US-CA"])
XCTAssertEqual(labels["ISO-639-1"], ["en"])
XCTAssertEqual(labels["cities"], ["Milan", "San Francisco"])
XCTAssertEqual(labels["ugc"], ["Italy", "United States of America"])
XCTAssertEqual(labels["doesnotexist"], nil)

try verifyEvent(event)
}

func testExpiration() throws {
let futureExpiration = Int64(Date.now.timeIntervalSince1970 + 10000)
let futureExpirationEvent = try NostrEvent.Builder(kind: .textNote)
.expiration(futureExpiration)
.build(signedBy: .test)
XCTAssertEqual(futureExpirationEvent.expiration, futureExpiration)
XCTAssertFalse(futureExpirationEvent.isExpired)

let pastExpiration = Int64(Date.now.timeIntervalSince1970 - 1)
let pastExpirationEvent = try NostrEvent.Builder(kind: .textNote)
.expiration(pastExpiration)
.build(signedBy: .test)
XCTAssertEqual(pastExpirationEvent.expiration, pastExpiration)
XCTAssertTrue(pastExpirationEvent.isExpired)

let decodedExpiredEvent: NostrEvent = try decodeFixture(filename: "test_event_expired")
XCTAssertEqual(decodedExpiredEvent.expiration, 1697090842)
XCTAssertTrue(decodedExpiredEvent.isExpired)
}

}
24 changes: 24 additions & 0 deletions Tests/NostrSDKTests/Events/Tags/AlternativeSummaryTagTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// AlternativeSummaryTagTests.swift
// NostrSDK
//
// Created by Terry Yiu on 11/3/24.
//

@testable import NostrSDK
import XCTest

final class AlternativeSummaryTagTests: XCTestCase, FixtureLoading {

func testAlternativeSummary() throws {
let alternativeSummary = "Alternative summary to display for clients that do not support this event kind."
let customEvent = try NostrEvent.Builder(kind: EventKind(rawValue: 23456))
.alternativeSummary(alternativeSummary)
.build(signedBy: .test)
XCTAssertEqual(customEvent.alternativeSummary, alternativeSummary)

let decodedCustomEventWithAltTag: NostrEvent = try decodeFixture(filename: "custom_event_alt_tag")
XCTAssertEqual(decodedCustomEventWithAltTag.alternativeSummary, alternativeSummary)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import XCTest

final class ContentWarningTagTests: XCTestCase, EventVerifying {

func testCreateContentWarningTaggedEvent() throws {
func testContentWarning() throws {
let event = try NostrEvent.Builder(kind: .textNote)
.contentWarning("Trigger warning.")
.content("Pineapple goes great on pizza.")
Expand Down
33 changes: 33 additions & 0 deletions Tests/NostrSDKTests/Events/Tags/ExpirationTagTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// ExpirationTagTests.swift
// NostrSDK
//
// Created by Terry Yiu on 11/3/24.
//

@testable import NostrSDK
import XCTest

final class ExpirationTagTests: XCTestCase, FixtureLoading {

func testExpiration() throws {
let futureExpiration = Int64(Date.now.timeIntervalSince1970 + 10000)
let futureExpirationEvent = try NostrEvent.Builder(kind: .textNote)
.expiration(futureExpiration)
.build(signedBy: .test)
XCTAssertEqual(futureExpirationEvent.expiration, futureExpiration)
XCTAssertFalse(futureExpirationEvent.isExpired)

let pastExpiration = Int64(Date.now.timeIntervalSince1970 - 1)
let pastExpirationEvent = try NostrEvent.Builder(kind: .textNote)
.expiration(pastExpiration)
.build(signedBy: .test)
XCTAssertEqual(pastExpirationEvent.expiration, pastExpiration)
XCTAssertTrue(pastExpirationEvent.isExpired)

let decodedExpiredEvent: NostrEvent = try decodeFixture(filename: "test_event_expired")
XCTAssertEqual(decodedExpiredEvent.expiration, 1697090842)
XCTAssertTrue(decodedExpiredEvent.isExpired)
}

}
41 changes: 41 additions & 0 deletions Tests/NostrSDKTests/Events/Tags/LabelTagTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// LabelTagTests.swift
// NostrSDK
//
// Created by Terry Yiu on 11/3/24.
//

@testable import NostrSDK
import XCTest

final class LabelTagTests: XCTestCase, EventVerifying {

func testLabels() throws {
let event = try TextNoteEvent.Builder()
.appendLabels("IT-MI", "US-CA", namespace: "ISO-3166-2")
.appendLabels("en", namespace: "ISO-639-1")
.appendLabels("Milan", "San Francisco", mark: "cities")
.appendLabels("Italy", "United States of America")
.content("It's beautiful here in Milan and wonderful there in San Francisco!")
.build(signedBy: .test)

XCTAssertEqual(event.labels(for: "ISO-3166-2"), ["IT-MI", "US-CA"])
XCTAssertEqual(event.labels(for: "ISO-639-1"), ["en"])
XCTAssertEqual(event.labels(for: "cities"), ["Milan", "San Francisco"])
XCTAssertEqual(event.labels(for: nil), ["Italy", "United States of America"])
XCTAssertEqual(event.labels(for: "ugc"), ["Italy", "United States of America"])
XCTAssertEqual(event.labels(for: "doesnotexist"), [])

XCTAssertEqual(event.labelNamespaces, ["ISO-3166-2", "ISO-639-1"])

let labels = event.labels
XCTAssertEqual(labels["ISO-3166-2"], ["IT-MI", "US-CA"])
XCTAssertEqual(labels["ISO-639-1"], ["en"])
XCTAssertEqual(labels["cities"], ["Milan", "San Francisco"])
XCTAssertEqual(labels["ugc"], ["Italy", "United States of America"])
XCTAssertEqual(labels["doesnotexist"], nil)

try verifyEvent(event)
}

}

0 comments on commit 7dcc12d

Please sign in to comment.