From 7dcc12df29c764fd8cf0d8f8b753a6df8d313bfc Mon Sep 17 00:00:00 2001 From: Terry Yiu <963907+tyiu@users.noreply.github.com> Date: Sun, 3 Nov 2024 18:07:44 +0100 Subject: [PATCH] Refactor out common non-NIP-01 tags out of NostrEvent file into separate protocols for better code separation (#197) --- Sources/NostrSDK/Events/LabelEvent.swift | 4 +- Sources/NostrSDK/Events/NostrEvent.swift | 48 +-------------- .../Events/Tags/AlternativeSummaryTag.swift | 33 ++++++++++ .../NostrSDK/Events/Tags/ExpirationTag.swift | 44 +++++++++++++ .../Events/NostrEventTests.swift | 61 +------------------ .../Tags/AlternativeSummaryTagTests.swift | 24 ++++++++ .../Events/Tags/ContentWarningTagTests.swift | 2 +- .../Events/Tags/ExpirationTagTests.swift | 33 ++++++++++ .../Events/Tags/LabelTagTests.swift | 41 +++++++++++++ 9 files changed, 181 insertions(+), 109 deletions(-) create mode 100644 Sources/NostrSDK/Events/Tags/AlternativeSummaryTag.swift create mode 100644 Sources/NostrSDK/Events/Tags/ExpirationTag.swift create mode 100644 Tests/NostrSDKTests/Events/Tags/AlternativeSummaryTagTests.swift create mode 100644 Tests/NostrSDKTests/Events/Tags/ExpirationTagTests.swift create mode 100644 Tests/NostrSDKTests/Events/Tags/LabelTagTests.swift diff --git a/Sources/NostrSDK/Events/LabelEvent.swift b/Sources/NostrSDK/Events/LabelEvent.swift index eb3123f..d5f5c1d 100644 --- a/Sources/NostrSDK/Events/LabelEvent.swift +++ b/Sources/NostrSDK/Events/LabelEvent.swift @@ -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. diff --git a/Sources/NostrSDK/Events/NostrEvent.swift b/Sources/NostrSDK/Events/NostrEvent.swift index 04079c3..a943e40 100644 --- a/Sources/NostrSDK/Events/NostrEvent.swift +++ b/Sources/NostrSDK/Events/NostrEvent.swift @@ -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 && @@ -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 } @@ -318,7 +291,7 @@ public protocol NostrEventBuilding { public extension NostrEvent { /// Builder of a ``NostrEvent`` of type `T`. - class Builder: NostrEventBuilding, ContentWarningTagBuilding, LabelBuilding { + class Builder: NostrEventBuilding, AlternativeSummaryTagBuilding, ContentWarningTagBuilding, ExpirationTagBuilding, LabelTagBuilding { public typealias EventType = T /// The event kind. @@ -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, diff --git a/Sources/NostrSDK/Events/Tags/AlternativeSummaryTag.swift b/Sources/NostrSDK/Events/Tags/AlternativeSummaryTag.swift new file mode 100644 index 0000000..ca534d1 --- /dev/null +++ b/Sources/NostrSDK/Events/Tags/AlternativeSummaryTag.swift @@ -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)) + } +} diff --git a/Sources/NostrSDK/Events/Tags/ExpirationTag.swift b/Sources/NostrSDK/Events/Tags/ExpirationTag.swift new file mode 100644 index 0000000..2df8a2f --- /dev/null +++ b/Sources/NostrSDK/Events/Tags/ExpirationTag.swift @@ -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))) + } +} diff --git a/Tests/NostrSDKTests/Events/NostrEventTests.swift b/Tests/NostrSDKTests/Events/NostrEventTests.swift index c271f86..59908b7 100644 --- a/Tests/NostrSDKTests/Events/NostrEventTests.swift +++ b/Tests/NostrSDKTests/Events/NostrEventTests.swift @@ -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") @@ -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) - } - } diff --git a/Tests/NostrSDKTests/Events/Tags/AlternativeSummaryTagTests.swift b/Tests/NostrSDKTests/Events/Tags/AlternativeSummaryTagTests.swift new file mode 100644 index 0000000..c5b3246 --- /dev/null +++ b/Tests/NostrSDKTests/Events/Tags/AlternativeSummaryTagTests.swift @@ -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) + } + +} diff --git a/Tests/NostrSDKTests/Events/Tags/ContentWarningTagTests.swift b/Tests/NostrSDKTests/Events/Tags/ContentWarningTagTests.swift index 111cc8c..09b248f 100644 --- a/Tests/NostrSDKTests/Events/Tags/ContentWarningTagTests.swift +++ b/Tests/NostrSDKTests/Events/Tags/ContentWarningTagTests.swift @@ -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.") diff --git a/Tests/NostrSDKTests/Events/Tags/ExpirationTagTests.swift b/Tests/NostrSDKTests/Events/Tags/ExpirationTagTests.swift new file mode 100644 index 0000000..98f7c5e --- /dev/null +++ b/Tests/NostrSDKTests/Events/Tags/ExpirationTagTests.swift @@ -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) + } + +} diff --git a/Tests/NostrSDKTests/Events/Tags/LabelTagTests.swift b/Tests/NostrSDKTests/Events/Tags/LabelTagTests.swift new file mode 100644 index 0000000..6221264 --- /dev/null +++ b/Tests/NostrSDKTests/Events/Tags/LabelTagTests.swift @@ -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) + } + +}