diff --git a/Sources/NostrSDK/CustomEmoji.swift b/Sources/NostrSDK/CustomEmoji.swift index cda86dd..8b9026b 100644 --- a/Sources/NostrSDK/CustomEmoji.swift +++ b/Sources/NostrSDK/CustomEmoji.swift @@ -39,6 +39,16 @@ public class CustomEmoji: CustomEmojiValidating, Equatable { } } +/// Builder that adds ``CustomEmoji``s to a ``NostrEvent``. +public protocol CustomEmojiBuilding: NostrEventBuilding {} +public extension CustomEmojiBuilding { + /// Adds a list of ``CustomEmoji``. + @discardableResult + func customEmojis(_ customEmojis: [CustomEmoji]) -> Self { + appendTags(contentsOf: customEmojis.map { $0.tag }) + } +} + public protocol CustomEmojiInterpreting: NostrEvent, CustomEmojiValidating {} public extension CustomEmojiInterpreting { /// Returns the list of well-formatted custom emojis derived from NostrEvent tags. diff --git a/Sources/NostrSDK/Events/AuthenticationEvent.swift b/Sources/NostrSDK/Events/AuthenticationEvent.swift index 8c68f4e..964899c 100644 --- a/Sources/NostrSDK/Events/AuthenticationEvent.swift +++ b/Sources/NostrSDK/Events/AuthenticationEvent.swift @@ -17,10 +17,20 @@ public final class AuthenticationEvent: NostrEvent, RelayProviding { } @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 { + required 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) } + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .authentication, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/BookmarksListEvent.swift b/Sources/NostrSDK/Events/BookmarksListEvent.swift index 91a9a4a..d5ae1d9 100644 --- a/Sources/NostrSDK/Events/BookmarksListEvent.swift +++ b/Sources/NostrSDK/Events/BookmarksListEvent.swift @@ -16,10 +16,20 @@ public final class BookmarksListEvent: NostrEvent, HashtagInterpreting, PrivateT } @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 { + required 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) } - + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .bookmarksList, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/Calendars/CalendarEventRSVP.swift b/Sources/NostrSDK/Events/Calendars/CalendarEventRSVP.swift index 15266ff..4105aaf 100644 --- a/Sources/NostrSDK/Events/Calendars/CalendarEventRSVP.swift +++ b/Sources/NostrSDK/Events/Calendars/CalendarEventRSVP.swift @@ -16,10 +16,20 @@ public final class CalendarEventRSVP: NostrEvent, ParameterizedReplaceableEvent } @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 { + required 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) } + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + public init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .calendarEventRSVP, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/Calendars/CalendarListEvent.swift b/Sources/NostrSDK/Events/Calendars/CalendarListEvent.swift index a3b7134..aee8275 100644 --- a/Sources/NostrSDK/Events/Calendars/CalendarListEvent.swift +++ b/Sources/NostrSDK/Events/Calendars/CalendarListEvent.swift @@ -18,10 +18,20 @@ public final class CalendarListEvent: NostrEvent, ParameterizedReplaceableEvent, } @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 { + required 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) } + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + public init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .calendar, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/Calendars/DateBasedCalendarEvent.swift b/Sources/NostrSDK/Events/Calendars/DateBasedCalendarEvent.swift index ce1d114..047c3a8 100644 --- a/Sources/NostrSDK/Events/Calendars/DateBasedCalendarEvent.swift +++ b/Sources/NostrSDK/Events/Calendars/DateBasedCalendarEvent.swift @@ -16,10 +16,20 @@ public final class DateBasedCalendarEvent: NostrEvent, CalendarEventInterpreting } @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 { + required 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) } + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + public init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .dateBasedCalendarEvent, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/Calendars/TimeBasedCalendarEvent.swift b/Sources/NostrSDK/Events/Calendars/TimeBasedCalendarEvent.swift index a38b874..8146003 100644 --- a/Sources/NostrSDK/Events/Calendars/TimeBasedCalendarEvent.swift +++ b/Sources/NostrSDK/Events/Calendars/TimeBasedCalendarEvent.swift @@ -14,10 +14,20 @@ public final class TimeBasedCalendarEvent: NostrEvent, CalendarEventInterpreting } @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 { + required 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) } + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + public init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .timeBasedCalendarEvent, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/DeletionEvent.swift b/Sources/NostrSDK/Events/DeletionEvent.swift index fc36a07..323a1c1 100644 --- a/Sources/NostrSDK/Events/DeletionEvent.swift +++ b/Sources/NostrSDK/Events/DeletionEvent.swift @@ -18,10 +18,20 @@ public final class DeletionEvent: NostrEvent, EventCoordinatesTagInterpreting { } @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 { + required 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) } - + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + 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) } diff --git a/Sources/NostrSDK/Events/FollowListEvent.swift b/Sources/NostrSDK/Events/FollowListEvent.swift index f42d198..3d6c21a 100644 --- a/Sources/NostrSDK/Events/FollowListEvent.swift +++ b/Sources/NostrSDK/Events/FollowListEvent.swift @@ -37,10 +37,20 @@ public final class FollowListEvent: NostrEvent, NonParameterizedReplaceableEvent } @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 { + required 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) } - + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + init(tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .followList, content: "", tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/GenericRepostEvent.swift b/Sources/NostrSDK/Events/GenericRepostEvent.swift index 18e1bcd..b2f2ac5 100644 --- a/Sources/NostrSDK/Events/GenericRepostEvent.swift +++ b/Sources/NostrSDK/Events/GenericRepostEvent.swift @@ -17,10 +17,20 @@ public class GenericRepostEvent: NostrEvent { } @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 { + required 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) } - + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + init(content: String, tags: [Tag], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: Self.kind, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/GiftWrap/GiftWrapEvent.swift b/Sources/NostrSDK/Events/GiftWrap/GiftWrapEvent.swift index d8f1356..c11729c 100644 --- a/Sources/NostrSDK/Events/GiftWrap/GiftWrapEvent.swift +++ b/Sources/NostrSDK/Events/GiftWrap/GiftWrapEvent.swift @@ -22,10 +22,20 @@ public final class GiftWrapEvent: NostrEvent, NIP44v2Encrypting { } @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 { + required 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) } + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + public init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970 - TimeInterval.random(in: 0...172800)), signedBy keypair: Keypair) throws { try super.init(kind: .giftWrap, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/GiftWrap/SealEvent.swift b/Sources/NostrSDK/Events/GiftWrap/SealEvent.swift index f9e6e10..3b1ebba 100644 --- a/Sources/NostrSDK/Events/GiftWrap/SealEvent.swift +++ b/Sources/NostrSDK/Events/GiftWrap/SealEvent.swift @@ -23,10 +23,20 @@ public final class SealEvent: NostrEvent, NIP44v2Encrypting { } @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 { + required 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) } + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + public init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970 - TimeInterval.random(in: 0...172800)), signedBy keypair: Keypair) throws { try super.init(kind: .seal, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/LegacyEncryptedDirectMessageEvent.swift b/Sources/NostrSDK/Events/LegacyEncryptedDirectMessageEvent.swift index e6587d6..01f24ee 100644 --- a/Sources/NostrSDK/Events/LegacyEncryptedDirectMessageEvent.swift +++ b/Sources/NostrSDK/Events/LegacyEncryptedDirectMessageEvent.swift @@ -19,10 +19,20 @@ public final class LegacyEncryptedDirectMessageEvent: NostrEvent, LegacyDirectMe } @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 { + required 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) } - + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .legacyEncryptedDirectMessage, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/LongformContentEvent.swift b/Sources/NostrSDK/Events/LongformContentEvent.swift index bbb3b93..83ad35e 100644 --- a/Sources/NostrSDK/Events/LongformContentEvent.swift +++ b/Sources/NostrSDK/Events/LongformContentEvent.swift @@ -20,10 +20,20 @@ public final class LongformContentEvent: NostrEvent, HashtagInterpreting, Parame } @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 { + required 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) } - + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .longformContent, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/MetadataEvent.swift b/Sources/NostrSDK/Events/MetadataEvent.swift index e711cd7..c5f2e85 100644 --- a/Sources/NostrSDK/Events/MetadataEvent.swift +++ b/Sources/NostrSDK/Events/MetadataEvent.swift @@ -98,10 +98,21 @@ public final class MetadataEvent: NostrEvent, CustomEmojiInterpreting, NonParame } @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 { + required 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) } - + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + + @available(*, deprecated, message: "Deprecated in favor of MetadataEvent.Builder.") init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .metadata, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } @@ -138,20 +149,45 @@ public extension EventCreating { /// > Note: If `rawUserMetadata` has fields that conflict with `userMetadata`, `userMetadata` fields take precedence. /// /// > Note: [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) + @available(*, deprecated, message: "Deprecated in favor of MetadataEvent.Builder.") func metadataEvent(withUserMetadata userMetadata: UserMetadata, rawUserMetadata: [String: Any] = [:], customEmojis: [CustomEmoji] = [], signedBy keypair: Keypair) throws -> MetadataEvent { - let userMetadataAsData = try JSONEncoder().encode(userMetadata) - - let allUserMetadataAsData: Data - if rawUserMetadata.isEmpty { - allUserMetadataAsData = userMetadataAsData - } else { - var userMetadataAsDictionary = try JSONSerialization.jsonObject(with: userMetadataAsData, options: []) as? [String: Any] ?? [:] - userMetadataAsDictionary.merge(rawUserMetadata) { (current, _) in current } - allUserMetadataAsData = try JSONSerialization.data(withJSONObject: userMetadataAsDictionary, options: .sortedKeys) + try MetadataEvent.Builder() + .userMetadata(userMetadata, merging: rawUserMetadata) + .customEmojis(customEmojis) + .build(signedBy: keypair) + } +} + +public extension MetadataEvent { + /// Builder of ``MetadataEvent``. + final class Builder: NostrEvent.Builder, CustomEmojiBuilding { + public init() { + super.init(kind: .metadata) } - let allUserMetadataAsString = String(decoding: allUserMetadataAsData, as: UTF8.self) - let customEmojiTags = customEmojis.map { $0.tag } - return try MetadataEvent(content: allUserMetadataAsString, tags: customEmojiTags, signedBy: keypair) + /// Sets the user metadata by merging ``UserMetadata`` with a dictionary of raw metadata. + /// + /// - Parameters: + /// - userMetadata: The ``UserMetadata`` to set. + /// - rawUserMetadata: The dictionary of raw metadata to set that can contain fields unknown to any implemented NIPs. + /// + /// > Note: If `rawUserMetadata` has fields that conflict with `userMetadata`, `userMetadata` fields take precedence. + public final func userMetadata(_ userMetadata: UserMetadata, merging rawUserMetadata: [String: Any] = [:]) throws -> Self { + let userMetadataAsData = try JSONEncoder().encode(userMetadata) + + let allUserMetadataAsData: Data + if rawUserMetadata.isEmpty { + allUserMetadataAsData = userMetadataAsData + } else { + var userMetadataAsDictionary = try JSONSerialization.jsonObject(with: userMetadataAsData, options: []) as? [String: Any] ?? [:] + userMetadataAsDictionary.merge(rawUserMetadata) { (current, _) in current } + allUserMetadataAsData = try JSONSerialization.data(withJSONObject: userMetadataAsDictionary, options: .sortedKeys) + } + + let allUserMetadataAsString = String(decoding: allUserMetadataAsData, as: UTF8.self) + content(allUserMetadataAsString) + + return self + } } } diff --git a/Sources/NostrSDK/Events/MuteListEvent.swift b/Sources/NostrSDK/Events/MuteListEvent.swift index 8e3518c..d70f37b 100644 --- a/Sources/NostrSDK/Events/MuteListEvent.swift +++ b/Sources/NostrSDK/Events/MuteListEvent.swift @@ -16,10 +16,20 @@ public final class MuteListEvent: NostrEvent, HashtagInterpreting, PrivateTagInt } @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 { + required 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) } - + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .muteList, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/NostrEvent.swift b/Sources/NostrSDK/Events/NostrEvent.swift index 3f5b0ce..2d50048 100644 --- a/Sources/NostrSDK/Events/NostrEvent.swift +++ b/Sources/NostrSDK/Events/NostrEvent.swift @@ -21,25 +21,25 @@ public class NostrEvent: Codable, Equatable, Hashable { lhs.signature == rhs.signature } - /// 32-byte, lowercase, hex-encoded sha256 of the serialized event data + /// 32-byte, lowercase, hex-encoded sha256 of the serialized event data. public let id: String - /// 32-byte, lowercase, hex-encoded public key of the event creator + /// 32-byte, lowercase, hex-encoded public key of the event creator. public let pubkey: String - /// unix timestamp in seconds + /// Unix timestamp in seconds of when the event is created. public let createdAt: Int64 - /// integer + /// The event kind. public let kind: EventKind - /// list of tags, see ``Tag`` + /// List of ``Tag`` objects. public let tags: [Tag] - /// arbitrary string + /// Arbitrary string. public let content: String - /// 64-byte hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field + /// 64-byte hex of the signature of the sha256 hash of the serialized event data, which is the same as the `id` field. public let signature: String? private enum CodingKeys: String, CodingKey { @@ -51,7 +51,7 @@ public class NostrEvent: Codable, Equatable, Hashable { case content case signature = "sig" } - + init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { self.id = id self.pubkey = pubkey @@ -62,7 +62,23 @@ public class NostrEvent: Codable, Equatable, Hashable { self.signature = signature } - init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { + /// Creates a ``NostrEvent`` rumor, which is an event with a `nil` signature. + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + self.kind = kind + self.content = content + self.tags = tags + self.createdAt = createdAt + self.pubkey = pubkey + id = EventSerializer.identifierForEvent(withPubkey: pubkey, + createdAt: createdAt, + kind: kind.rawValue, + tags: tags, + content: content) + signature = nil + } + + /// Creates a signed ``NostrEvent``. + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { self.kind = kind self.content = content self.tags = tags @@ -184,3 +200,163 @@ extension NostrEvent: MetadataCoding, RelayURLValidating { return try encodedIdentifier(with: metadata, identifierType: .event) } } + +/// This protocol describes a builder that is able to build a ``NostrEvent``. +public protocol NostrEventBuilding { + /// The type of ``NostrEvent`` that this builder constructs. + associatedtype EventType: NostrEvent + + /// Sets the unix timestamp in seconds of when the event is created. + func createdAt(_ createdAt: Int64?) -> Self + + /// Appends the given list of tags to the end of the existing tags list. + /// - Parameters: + /// - tags: The list of ``Tag`` objects. + func appendTags(_ tags: Tag...) -> Self + + /// Appends the given list of tags to the end of the existing tags list. + /// - Parameters: + /// - tags: The list of ``Tag`` objects. + func appendTags(contentsOf tags: [Tag]) -> Self + + /// Inserts the given list of tags at a given index of the list. + /// - Parameters: + /// - tags: The list of `Tag` objects to insert. + /// - index: The index of the existing list to insert the new tags into. + /// The tags are appended to the end of the list if the index is `nil`. + /// Must be a valid index of the existing tags list. + func insertTags(_ tags: Tag..., at index: Int) -> Self + + /// Inserts the given list of tags at a given index of the list. + /// - Parameters: + /// - tags: The list of `Tag` objects to insert. + /// - index: The index of the existing list to insert the new tags into. + /// The tags are appended to the end of the list if the index is `nil`. + /// Must be a valid index of the existing tags list. + func insertTags(contentsOf tags: [Tag], at index: Int) -> Self + + /// Arbitrary string. + func content(_ content: String?) -> Self + + /// Builds a ``NostrEvent`` of type ``EventType`` using the properties set on the builder and signs the event. + /// + /// If `createdAt` is not set, the current timestamp is used. + /// If `content` is not set, an empty string is used. + /// + /// - Parameter keypair: The ``Keypair`` to sign the event. + /// + /// Throws an error if the event could not be signed with the given keypair. + func build(signedBy keypair: Keypair) throws -> EventType + + /// Builds a ``NostrEvent`` of type ``EventType`` using the properties set on the builder and does not sign the event, + /// also known as a rumor event. + /// + /// If `createdAt` is not set, the current timestamp is used. + /// If `content` is not set, an empty string is used. + /// + /// - Parameter pubkey: The ``PublicKey`` of the event creator. + func build(pubkey: PublicKey) -> EventType + + /// Builds a ``NostrEvent`` of type ``EventType`` using the properties set on the builder and does not sign the event, + /// also known as a rumor event. + /// + /// If `createdAt` is not set, the current timestamp is used. + /// If `content` is not set, an empty string is used. + /// + /// - Parameter pubkey: The 32-byte, lowercase, hex-encoded public key of the event creator. + func build(pubkey: String) -> EventType +} + +public extension NostrEvent { + /// Builder of a ``NostrEvent`` of type `T`. + class Builder: NostrEventBuilding { + public typealias EventType = T + + /// The event kind. + public final let kind: EventKind + + /// The unix timestamp in seconds of when the event is created. + public private(set) final var createdAt: Int64? + + /// Arbitrary string. + public private(set) final var content: String? + + /// List of ``Tag``s. + public private(set) final var tags: [Tag] = [] + + /// Creates a ``Builder`` from an ``EventKind``. + public init(kind: EventKind) { + self.kind = kind + } + + /// Creates a ``Builder`` from a ``NostrEvent`` + /// by copying the `kind`, `tags`, and `content` properties into the builder. + /// The `pubkey`, `createdAt`, and `signature` properties are not copied + /// because they are computed upon building the final event. + public init(nostrEvent: NostrEvent) { + self.kind = nostrEvent.kind + self.tags = nostrEvent.tags + self.content = nostrEvent.content + } + + @discardableResult + public final func createdAt(_ createdAt: Int64?) -> Self { + self.createdAt = createdAt + return self + } + + @discardableResult + public final func appendTags(_ tags: Tag...) -> Self { + appendTags(contentsOf: tags) + return self + } + + @discardableResult + public final func appendTags(contentsOf tags: [Tag]) -> Self { + self.tags.append(contentsOf: tags) + return self + } + + @discardableResult + public final func insertTags(_ tags: Tag..., at index: Int) -> Self { + insertTags(contentsOf: tags, at: index) + return self + } + + @discardableResult + public final func insertTags(contentsOf tags: [Tag], at index: Int) -> Self { + self.tags.insert(contentsOf: tags, at: index) + return self + } + + @discardableResult + public final func content(_ content: String?) -> Self { + self.content = content + return self + } + + public final func build(signedBy keypair: Keypair) throws -> T { + try T( + kind: kind, + content: content ?? "", + tags: tags, + createdAt: createdAt ?? Int64(Date.now.timeIntervalSince1970), + signedBy: keypair + ) + } + + public final func build(pubkey: PublicKey) -> T { + build(pubkey: pubkey.hex) + } + + public final func build(pubkey: String) -> T { + T( + kind: kind, + content: content ?? "", + tags: tags, + createdAt: createdAt ?? Int64(Date.now.timeIntervalSince1970), + pubkey: pubkey + ) + } + } +} diff --git a/Sources/NostrSDK/Events/ReactionEvent.swift b/Sources/NostrSDK/Events/ReactionEvent.swift index 2e56297..69a6fc7 100644 --- a/Sources/NostrSDK/Events/ReactionEvent.swift +++ b/Sources/NostrSDK/Events/ReactionEvent.swift @@ -17,10 +17,20 @@ public class ReactionEvent: NostrEvent, CustomEmojiInterpreting { } @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 { + required 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) } - + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .reaction, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/RelayListMetadataEvent.swift b/Sources/NostrSDK/Events/RelayListMetadataEvent.swift index 11a74ff..82433c8 100644 --- a/Sources/NostrSDK/Events/RelayListMetadataEvent.swift +++ b/Sources/NostrSDK/Events/RelayListMetadataEvent.swift @@ -27,10 +27,20 @@ public final class RelayListMetadataEvent: NostrEvent, NonParameterizedReplaceab } @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 { + required 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) } + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + init(tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .relayListMetadata, content: "", tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/ReportEvent.swift b/Sources/NostrSDK/Events/ReportEvent.swift index 429d2f6..a66c332 100644 --- a/Sources/NostrSDK/Events/ReportEvent.swift +++ b/Sources/NostrSDK/Events/ReportEvent.swift @@ -35,10 +35,20 @@ public final class ReportEvent: NostrEvent { } @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 { + required 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) } - + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + public init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .report, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/Tags/EventTag.swift b/Sources/NostrSDK/Events/Tags/EventTag.swift index 4079d3b..c4e7009 100644 --- a/Sources/NostrSDK/Events/Tags/EventTag.swift +++ b/Sources/NostrSDK/Events/Tags/EventTag.swift @@ -96,6 +96,16 @@ public struct EventTag: RelayProviding, RelayURLValidating, Equatable { return EventTagMarker(rawValue: tag.otherParameters[1]) } + /// The pubkey of the author of the referenced event. + public var pubkey: String? { + guard tag.otherParameters.count >= 3 else { + return nil + } + + // Validate that the pubkey is valid before returning it. + return PublicKey(hex: tag.otherParameters[2])?.hex + } + /// Initializes an event tag from a ``Tag``. /// `nil` is returned if the tag is not an event tag. public init?(tag: Tag) { @@ -111,7 +121,7 @@ public struct EventTag: RelayProviding, RelayURLValidating, Equatable { /// - eventId: The id of the event being referenced. /// - relayURL: The URL of a recommended relay associated with the reference. /// - marker: The marker indicating the type of event tag. - public init(eventId: String, relayURL: URL? = nil, marker: EventTagMarker?) throws { + public init(eventId: String, relayURL: URL? = nil, marker: EventTagMarker? = nil, pubkey: String? = nil) throws { let validatedRelayURL: URL? if let relayURL { validatedRelayURL = try RelayURLValidator.shared.validateRelayURL(relayURL) @@ -119,14 +129,24 @@ public struct EventTag: RelayProviding, RelayURLValidating, Equatable { validatedRelayURL = nil } + if let pubkey, PublicKey(hex: pubkey) == nil { + throw EventCreatingError.invalidInput + } + + var tagOtherParameters = [validatedRelayURL?.absoluteString ?? ""] + if let marker { guard marker == .root || marker == .reply || marker == .mention else { throw EventTagError.invalidInput } - tag = Tag(name: .event, value: eventId, otherParameters: [validatedRelayURL?.absoluteString ?? "", marker.rawValue]) - } else { - tag = Tag(name: .event, value: eventId, otherParameters: [validatedRelayURL?.absoluteString ?? ""]) + tagOtherParameters.append(marker.rawValue) } + + if let pubkey { + tagOtherParameters.append(pubkey) + } + + tag = Tag(name: .event, value: eventId, otherParameters: tagOtherParameters) } } diff --git a/Sources/NostrSDK/Events/TextNoteEvent.swift b/Sources/NostrSDK/Events/TextNoteEvent.swift index a9c1f77..bd73667 100644 --- a/Sources/NostrSDK/Events/TextNoteEvent.swift +++ b/Sources/NostrSDK/Events/TextNoteEvent.swift @@ -15,16 +15,27 @@ public final class TextNoteEvent: NostrEvent, CustomEmojiInterpreting { 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 { + override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { + super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { + super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) + } + + @available(*, unavailable, message: "This initializer is unavailable for this class.") + required 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) } - + + @available(*, deprecated, message: "Deprecated in favor of TextNote.Builder.") init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { try super.init(kind: .textNote, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) } - + /// Pubkeys mentioned in the note content. public var mentionedPubkeys: [String] { allValues(forTagName: .pubkey) @@ -131,66 +142,103 @@ public extension EventCreating { /// /// See [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) /// See [NIP-10 - On "e" and "p" tags in Text Events (kind 1)](https://github.com/nostr-protocol/nips/blob/master/10.md) + @available(*, deprecated, message: "Deprecated in favor of TextNote.Builder.") func textNote(withContent content: String, replyingTo repliedEvent: TextNoteEvent? = nil, mentionedEventTags: [EventTag]? = nil, subject: String? = nil, customEmojis: [CustomEmoji]? = nil, signedBy keypair: Keypair) throws -> TextNoteEvent { + + let builder = TextNoteEvent.Builder() + .content(content) + .subject(subject) + + if let repliedEvent { + try builder.repliedEvent(repliedEvent) + } + + if let customEmojis { + builder.customEmojis(customEmojis) + } + if let mentionedEventTags { - guard mentionedEventTags.allSatisfy({ $0.marker == .mention }) else { - throw EventCreatingError.invalidInput - } + try builder.mentionedEventTags(mentionedEventTags) } - var tags: [Tag] = [] + return try builder.build(signedBy: keypair) + } +} - if let repliedEvent { +public extension TextNoteEvent { + /// Builder of a ``TextNoteEvent``. + final class Builder: NostrEvent.Builder, CustomEmojiBuilding { + public init() { + super.init(kind: .textNote) + } + + /// Sets the ``TextNoteEvent`` that is being replied to from this text note that is being built. + @discardableResult + public final func repliedEvent(_ repliedEvent: TextNoteEvent, relayURL: URL? = nil) throws -> Self { if let rootEventTag = repliedEvent.rootEventTag { // Maximize backwards compatibility with NIP-10 deprecated positional event tags // by ensuring ordering of types of event tags. - // 1. Root tag comes first. + // Root tag comes first. if rootEventTag.marker == .root { - tags.append(rootEventTag.tag) + insertTags(rootEventTag.tag, at: 0) } else { // Recreate the event tag with a root marker if the one being read does not have a marker. - let rootEventTagWithMarker = try EventTag(eventId: rootEventTag.eventId, relayURL: rootEventTag.relayURL, marker: .root) - tags.append(rootEventTagWithMarker.tag) - } - - // 2. Mentions go in between. - if let mentionedEventTags { - tags += mentionedEventTags.map { $0.tag } + let rootEventTagWithMarker = try EventTag(eventId: rootEventTag.eventId, relayURL: rootEventTag.relayURL, marker: .root, pubkey: rootEventTag.pubkey) + insertTags(rootEventTagWithMarker.tag, at: 0) } - // 3. Reply tag comes last. - tags.append(try EventTag(eventId: repliedEvent.id, marker: .reply).tag) - - // When replying to a text event E, the reply event's "p" tags should contain all of E's "p" tags as well as the "pubkey" of the event being replied to. - // Example: Given a text event authored by a1 with "p" tags [p1, p2, p3] then the "p" tags of the reply should be [a1, p1, p2, p3] in no particular order. - tags += repliedEvent.tags.filter { $0.name == TagName.pubkey.rawValue } - - // Add the author "p" tag if it was not already added. - if !tags.contains(where: { $0.name == TagName.pubkey.rawValue && $0.value == repliedEvent.pubkey }) { - tags.append(Tag(name: .pubkey, value: repliedEvent.pubkey)) - } + // Reply tag comes last. + appendTags(try EventTag(eventId: repliedEvent.id, relayURL: relayURL, marker: .reply, pubkey: repliedEvent.pubkey).tag) } else { - if let mentionedEventTags { - tags += mentionedEventTags.map { $0.tag } - } - // If the event being replied to has no root marker event tag, // the event being replied to is the root. - tags.append(try EventTag(eventId: repliedEvent.id, marker: .root).tag) + insertTags(try EventTag(eventId: repliedEvent.id, relayURL: relayURL, marker: .root, pubkey: repliedEvent.pubkey).tag, at: 0) } - } else if let mentionedEventTags { - tags += mentionedEventTags.map { $0.tag } - } - if let customEmojis { - tags += customEmojis.map { $0.tag } + // When replying to a text event E, the reply event's "p" tags should contain all of E's "p" tags as well as the "pubkey" of the event being replied to. + // Example: Given a text event authored by a1 with "p" tags [p1, p2, p3] then the "p" tags of the reply should be [a1, p1, p2, p3] in no particular order. + appendTags(contentsOf: repliedEvent.tags.filter { $0.name == TagName.pubkey.rawValue }) + + // Add the author "p" tag if it was not already added. + if !tags.contains(where: { $0.name == TagName.pubkey.rawValue && $0.value == repliedEvent.pubkey }) { + appendTags(Tag(name: .pubkey, value: repliedEvent.pubkey)) + } + + return self } - if let subject { - tags.append(Tag(name: .subject, value: subject)) + /// Sets the list of events, represented by ``EventTag``, that are mentioned from this text note that is being built. + @discardableResult + public final func mentionedEventTags(_ mentionedEventTags: [EventTag]) throws -> Builder { + guard !mentionedEventTags.isEmpty else { + return self + } + + guard mentionedEventTags.allSatisfy({ $0.marker == .mention }) else { + throw EventCreatingError.invalidInput + } + + let newTags = mentionedEventTags.map { $0.tag } + // Mentions go in between root markers and reply markers. + if let replyMarkerIndex = tags.firstIndex(where: { $0.otherParameters.count >= 2 && $0.otherParameters[1] == EventTagMarker.reply.rawValue }) { + insertTags(contentsOf: newTags, at: replyMarkerIndex) + } else { + appendTags(contentsOf: newTags) + } + + return self } - return try TextNoteEvent(content: content, tags: tags, signedBy: keypair) + /// Sets the subject for this text note. + @discardableResult + public final func subject(_ subject: String?) -> Builder { + guard let subject else { + return self + } + + appendTags(Tag(name: .subject, value: subject)) + return self + } } } diff --git a/Tests/NostrSDKTests/Events/GiftWrap/GiftWrapEventTests.swift b/Tests/NostrSDKTests/Events/GiftWrap/GiftWrapEventTests.swift index 157d6a4..9cfeeb4 100644 --- a/Tests/NostrSDKTests/Events/GiftWrap/GiftWrapEventTests.swift +++ b/Tests/NostrSDKTests/Events/GiftWrap/GiftWrapEventTests.swift @@ -26,7 +26,9 @@ final class GiftWrapEventTests: XCTestCase, EventCreating, EventVerifying, Fixtu } func testCreateGiftWrapFailsWithSignedEvent() throws { - let signedEvent = try textNote(withContent: "Are you going to the party tonight?", signedBy: .test) + let signedEvent = try TextNoteEvent.Builder() + .content("Are you going to the party tonight?") + .build(signedBy: .test) XCTAssertThrowsError(try giftWrap(withRumor: signedEvent, toRecipient: GiftWrapEventTests.recipient.publicKey, signedBy: .test)) } diff --git a/Tests/NostrSDKTests/Events/GiftWrap/RumorEventTests.swift b/Tests/NostrSDKTests/Events/GiftWrap/RumorEventTests.swift index 872af81..84012c3 100644 --- a/Tests/NostrSDKTests/Events/GiftWrap/RumorEventTests.swift +++ b/Tests/NostrSDKTests/Events/GiftWrap/RumorEventTests.swift @@ -11,7 +11,25 @@ import XCTest final class RumorEventTests: XCTestCase, EventCreating, EventVerifying, FixtureLoading { func testCreateRumor() throws { - let signedEvent = try textNote(withContent: "Are you going to the party tonight?", signedBy: .test) + let signedEvent = TextNoteEvent.Builder() + .content("Are you going to the party tonight?") + .build(pubkey: Keypair.test.publicKey) + let rumor = signedEvent.rumor + + XCTAssertEqual(rumor.pubkey, Keypair.test.publicKey.hex) + XCTAssertEqual(rumor.kind, .textNote) + XCTAssertEqual(rumor.tags, []) + XCTAssertNil(rumor.signature) + XCTAssertTrue(rumor.isRumor) + XCTAssertEqual(rumor.content, "Are you going to the party tonight?") + + XCTAssertThrowsError(try verifyEvent(rumor)) + } + + func testCreateRumorFromSignedEvent() throws { + let signedEvent = try TextNoteEvent.Builder() + .content("Are you going to the party tonight?") + .build(signedBy: .test) let rumor = signedEvent.rumor XCTAssertEqual(rumor.pubkey, Keypair.test.publicKey.hex) diff --git a/Tests/NostrSDKTests/Events/GiftWrap/SealEventTests.swift b/Tests/NostrSDKTests/Events/GiftWrap/SealEventTests.swift index 795e29d..6793205 100644 --- a/Tests/NostrSDKTests/Events/GiftWrap/SealEventTests.swift +++ b/Tests/NostrSDKTests/Events/GiftWrap/SealEventTests.swift @@ -26,7 +26,9 @@ final class SealEventTests: XCTestCase, EventCreating, EventVerifying, FixtureLo } func testCreateSealFailsWithSignedEvent() throws { - let signedEvent = try textNote(withContent: "Are you going to the party tonight?", signedBy: .test) + let signedEvent = try TextNoteEvent.Builder() + .content("Are you going to the party tonight?") + .build(signedBy: .test) XCTAssertThrowsError(try seal(withRumor: signedEvent, toRecipient: SealEventTests.recipient.publicKey, signedBy: .test)) } diff --git a/Tests/NostrSDKTests/Events/MetadataEventTests.swift b/Tests/NostrSDKTests/Events/MetadataEventTests.swift index 613f62f..e624176 100644 --- a/Tests/NostrSDKTests/Events/MetadataEventTests.swift +++ b/Tests/NostrSDKTests/Events/MetadataEventTests.swift @@ -42,7 +42,70 @@ final class MetadataEventTests: XCTestCase, EventCreating, EventVerifying, Fixtu Tag(name: .emoji, value: "apple", otherParameters: ["https://nostrsdk.com/apple.png"]) ] - let event = try metadataEvent(withUserMetadata: meta, rawUserMetadata: rawUserMetadata, customEmojis: customEmojis, signedBy: Keypair.test) + let event = try XCTUnwrap( + MetadataEvent.Builder() + .userMetadata(meta, merging: rawUserMetadata) + .customEmojis(customEmojis) + .build(signedBy: .test) + ) + + let expectedReplaceableEventCoordinates = try XCTUnwrap(EventCoordinates(kind: .metadata, pubkey: Keypair.test.publicKey)) + + XCTAssertEqual(event.userMetadata?.name, "Nostr SDK Test :ostrich:") + XCTAssertEqual(event.userMetadata?.displayName, "Nostr SDK Display Name") + XCTAssertEqual(event.userMetadata?.about, "I'm a test account. I'm used to test the Nostr SDK for Apple platforms. :apple:") + XCTAssertEqual(event.userMetadata?.website, URL(string: "https://github.com/nostr-sdk/nostr-sdk-ios")) + XCTAssertEqual(event.userMetadata?.nostrAddress, "test@nostr.com") + XCTAssertEqual(event.userMetadata?.pictureURL, URL(string: "https://nostrsdk.com/picture.png")) + XCTAssertEqual(event.userMetadata?.bannerPictureURL, URL(string: "https://nostrsdk.com/banner.png")) + XCTAssertEqual(event.userMetadata?.isBot, true) + XCTAssertEqual(event.userMetadata?.lightningURLString, "LNURL1234567890") + XCTAssertEqual(event.userMetadata?.lightningAddress, "satoshi@bitcoin.org") + XCTAssertEqual(event.rawUserMetadata["foo"] as? String, "string") + XCTAssertEqual(event.rawUserMetadata["bool"] as? Bool, true) + XCTAssertEqual(event.rawUserMetadata["number"] as? Int, 123) + XCTAssertEqual(event.rawUserMetadata["name"] as? String, "Nostr SDK Test :ostrich:") + XCTAssertEqual(event.rawUserMetadata["lud16"] as? String, "satoshi@bitcoin.org") + XCTAssertEqual(event.customEmojis, customEmojis) + XCTAssertEqual(event.replaceableEventCoordinates(relayURL: nil), expectedReplaceableEventCoordinates) + XCTAssertEqual(event.tags, customEmojiTags) + + try verifyEvent(event) + } + + func testCreateMetadataEventDeprecated() throws { + let meta = UserMetadata(name: "Nostr SDK Test :ostrich:", + displayName: "Nostr SDK Display Name", + about: "I'm a test account. I'm used to test the Nostr SDK for Apple platforms. :apple:", + website: URL(string: "https://github.com/nostr-sdk/nostr-sdk-ios"), + nostrAddress: "test@nostr.com", + pictureURL: URL(string: "https://nostrsdk.com/picture.png"), + bannerPictureURL: URL(string: "https://nostrsdk.com/banner.png"), + isBot: true, + lightningURLString: "LNURL1234567890", + lightningAddress: "satoshi@bitcoin.org") + + let rawUserMetadata: [String: Any] = [ + "foo": "string", + "bool": true, + "number": 123, + "name": "This field should be ignored.", + "lud16": "should@be.ignored" + ] + + let ostrichImageURL = try XCTUnwrap(URL(string: "https://nostrsdk.com/ostrich.png")) + let appleImageURL = try XCTUnwrap(URL(string: "https://nostrsdk.com/apple.png")) + + let customEmojis = [ + try XCTUnwrap(CustomEmoji(shortcode: "ostrich", imageURL: ostrichImageURL)), + try XCTUnwrap(CustomEmoji(shortcode: "apple", imageURL: appleImageURL)) + ] + let customEmojiTags = [ + Tag(name: .emoji, value: "ostrich", otherParameters: ["https://nostrsdk.com/ostrich.png"]), + Tag(name: .emoji, value: "apple", otherParameters: ["https://nostrsdk.com/apple.png"]) + ] + + let event = try metadataEvent(withUserMetadata: meta, rawUserMetadata: rawUserMetadata, customEmojis: customEmojis, signedBy: .test) let expectedReplaceableEventCoordinates = try XCTUnwrap(EventCoordinates(kind: .metadata, pubkey: Keypair.test.publicKey)) XCTAssertEqual(event.userMetadata?.name, "Nostr SDK Test :ostrich:") diff --git a/Tests/NostrSDKTests/Events/ReactionEventTests.swift b/Tests/NostrSDKTests/Events/ReactionEventTests.swift index 76895a9..886f3b8 100644 --- a/Tests/NostrSDKTests/Events/ReactionEventTests.swift +++ b/Tests/NostrSDKTests/Events/ReactionEventTests.swift @@ -11,11 +11,12 @@ import XCTest final class ReactionEventTests: XCTestCase, EventCreating, EventVerifying, FixtureLoading { func testCreateReactionEvent() throws { - let reactedEvent = try textNote(withContent: "Hello world!", - signedBy: Keypair.test) + let reactedEvent = try TextNoteEvent.Builder() + .content("Hello world!") + .build(signedBy: Keypair.test) let event = try reaction(withContent: "🤙", reactedEvent: reactedEvent, - signedBy: Keypair.test) + signedBy: .test) XCTAssertEqual(event.kind, .reaction) XCTAssertEqual(event.pubkey, Keypair.test.publicKey.hex) @@ -33,15 +34,15 @@ final class ReactionEventTests: XCTestCase, EventCreating, EventVerifying, Fixtu } func testCreateCustomEmojiReactionEvent() throws { - let reactedEvent = try textNote(withContent: "Hello world!", - signedBy: Keypair.test) + let reactedEvent = try TextNoteEvent.Builder() + .build(signedBy: .test) let imageURLString = "https://nostrsdk.com/ostrich.png" let imageURL = try XCTUnwrap(URL(string: imageURLString)) let customEmoji = try XCTUnwrap(CustomEmoji(shortcode: "ostrich", imageURL: imageURL)) let event = try reaction(withCustomEmoji: customEmoji, reactedEvent: reactedEvent, - signedBy: Keypair.test) + signedBy: .test) XCTAssertEqual(event.kind, .reaction) XCTAssertEqual(event.pubkey, Keypair.test.publicKey.hex) diff --git a/Tests/NostrSDKTests/Events/TextNoteEventTests.swift b/Tests/NostrSDKTests/Events/TextNoteEventTests.swift index 3de71d2..59184dc 100644 --- a/Tests/NostrSDKTests/Events/TextNoteEventTests.swift +++ b/Tests/NostrSDKTests/Events/TextNoteEventTests.swift @@ -15,28 +15,192 @@ final class TextNoteEventTests: XCTestCase, EventCreating, EventVerifying, Fixtu let imageURL = try XCTUnwrap(URL(string: imageURLString)) let customEmoji = try XCTUnwrap(CustomEmoji(shortcode: "ostrich", imageURL: imageURL)) + let note = try XCTUnwrap( + TextNoteEvent.Builder() + .content("Hello world! :ostrich:") + .customEmojis([customEmoji]) + .subject("test-subject") + .build(signedBy: .test) + ) + + XCTAssertEqual(note.kind, .textNote) + XCTAssertEqual(note.content, "Hello world! :ostrich:") + XCTAssertEqual(note.subject, "test-subject") + XCTAssertEqual(note.pubkey, Keypair.test.publicKey.hex) + XCTAssertEqual(note.tags, [Tag(name: .emoji, value: "ostrich", otherParameters: [imageURLString]), Tag(name: .subject, value: "test-subject")]) + XCTAssertEqual(note.customEmojis, [customEmoji]) + XCTAssertNil(note.rootEventTag) + XCTAssertNil(note.replyEventTag) + XCTAssertEqual(note.mentionedEventTags, []) + XCTAssertEqual(note.mentionedEventIds, []) + XCTAssertEqual(note.mentionedPubkeys, []) + + try verifyEvent(note) + } + + func testCreateSignedTextNoteDeprecated() throws { + let imageURLString = "https://nostrsdk.com/ostrich.png" + let imageURL = try XCTUnwrap(URL(string: imageURLString)) + let customEmoji = try XCTUnwrap(CustomEmoji(shortcode: "ostrich", imageURL: imageURL)) + let note = try textNote(withContent: "Hello world! :ostrich:", subject: "test-subject", customEmojis: [customEmoji], - signedBy: Keypair.test) + signedBy: .test) XCTAssertEqual(note.kind, .textNote) XCTAssertEqual(note.content, "Hello world! :ostrich:") XCTAssertEqual(note.subject, "test-subject") XCTAssertEqual(note.pubkey, Keypair.test.publicKey.hex) - XCTAssertEqual(note.tags, [Tag(name: .emoji, value: "ostrich", otherParameters: [imageURLString]), Tag(name: .subject, value: "test-subject")]) + XCTAssertEqual(note.tags, [Tag(name: .subject, value: "test-subject"), Tag(name: .emoji, value: "ostrich", otherParameters: [imageURLString])]) XCTAssertEqual(note.customEmojis, [customEmoji]) + XCTAssertNil(note.rootEventTag) + XCTAssertNil(note.replyEventTag) + XCTAssertEqual(note.mentionedEventTags, []) + XCTAssertEqual(note.mentionedEventIds, []) + XCTAssertEqual(note.mentionedPubkeys, []) + + try verifyEvent(note) + } + + func testCreateTextNoteTopLevelReply() throws { + let rootEvent = try TextNoteEvent.Builder() + .content("This is the note to reply.") + .build(signedBy: .test) + let rootRelayURL = try XCTUnwrap(URL(string: "wss://relay.damus.io")) + + let replyRelayURL = try XCTUnwrap(URL(string: "wss://relay.nostr.com")) + let mentionedEventTag1 = try XCTUnwrap(EventTag(eventId: "mentionednote1", relayURL: replyRelayURL, marker: .mention)) + let mentionedEventTag2 = try XCTUnwrap(EventTag(eventId: "mentionednote2", relayURL: replyRelayURL, marker: .mention)) + + let note = try XCTUnwrap( + TextNoteEvent.Builder() + .content("This is a reply to a note in a thread.") + .repliedEvent(rootEvent, relayURL: rootRelayURL) + .mentionedEventTags([mentionedEventTag1, mentionedEventTag2]) + .build(signedBy: .test) + ) + + XCTAssertEqual(note.kind, .textNote) + XCTAssertEqual(note.content, "This is a reply to a note in a thread.") + XCTAssertEqual(note.pubkey, Keypair.test.publicKey.hex) + + let rootEventTag = try XCTUnwrap(note.rootEventTag) + XCTAssertEqual(rootEventTag.eventId, rootEvent.id) + XCTAssertEqual(rootEventTag.relayURL, rootRelayURL) + XCTAssertEqual(rootEventTag.marker, .root) + XCTAssertEqual(rootEventTag.pubkey, Keypair.test.publicKey.hex) + + let replyEventTag = try XCTUnwrap(note.replyEventTag) + XCTAssertEqual(replyEventTag.eventId, rootEvent.id) + XCTAssertEqual(replyEventTag.relayURL, rootRelayURL) + XCTAssertEqual(replyEventTag.marker, .root) + XCTAssertEqual(replyEventTag.pubkey, Keypair.test.publicKey.hex) + + XCTAssertEqual(note.mentionedEventTags.count, 2) + XCTAssertEqual(note.mentionedEventTags[0], mentionedEventTag1) + XCTAssertEqual(note.mentionedEventTags[1], mentionedEventTag2) + + XCTAssertEqual(note.mentionedEventIds, [rootEvent.id, "mentionednote1", "mentionednote2"]) + XCTAssertEqual(note.mentionedPubkeys, [Keypair.test.publicKey.hex]) + + let expectedTags: [Tag] = [ + rootEventTag.tag, + .pubkey(Keypair.test.publicKey.hex), + mentionedEventTag1.tag, + mentionedEventTag2.tag + ] + XCTAssertEqual(note.tags, expectedTags) + + try verifyEvent(note) + } + + func testCreateTextNoteTopLevelReplyDeprecated() throws { + let rootEvent: TextNoteEvent = try textNote(withContent: "This is the note to reply.", signedBy: .test) + + let replyRelayURL = try XCTUnwrap(URL(string: "wss://relay.nostr.com")) + let mentionedEventTag1 = try XCTUnwrap(EventTag(eventId: "mentionednote1", relayURL: replyRelayURL, marker: .mention)) + let mentionedEventTag2 = try XCTUnwrap(EventTag(eventId: "mentionednote2", relayURL: replyRelayURL, marker: .mention)) + + let note = try textNote(withContent: "This is a reply to a note in a thread.", replyingTo: rootEvent, mentionedEventTags: [mentionedEventTag1, mentionedEventTag2], signedBy: .test) + + XCTAssertEqual(note.kind, .textNote) + XCTAssertEqual(note.content, "This is a reply to a note in a thread.") + XCTAssertEqual(note.pubkey, Keypair.test.publicKey.hex) + + let rootEventTag = try XCTUnwrap(note.rootEventTag) + XCTAssertEqual(rootEventTag.eventId, rootEvent.id) + XCTAssertNil(rootEventTag.relayURL) + XCTAssertEqual(rootEventTag.marker, .root) + XCTAssertEqual(rootEventTag.pubkey, Keypair.test.publicKey.hex) + + let replyEventTag = try XCTUnwrap(note.replyEventTag) + XCTAssertEqual(replyEventTag.eventId, rootEvent.id) + XCTAssertNil(replyEventTag.relayURL) + XCTAssertEqual(replyEventTag.marker, .root) + XCTAssertEqual(replyEventTag.pubkey, Keypair.test.publicKey.hex) + + XCTAssertEqual(note.mentionedEventTags.count, 2) + XCTAssertEqual(note.mentionedEventTags[0], mentionedEventTag1) + XCTAssertEqual(note.mentionedEventTags[1], mentionedEventTag2) + + XCTAssertEqual(note.mentionedEventIds, [rootEvent.id, "mentionednote1", "mentionednote2"]) + XCTAssertEqual(note.mentionedPubkeys, [Keypair.test.publicKey.hex]) + + let expectedTags: [Tag] = [ + rootEventTag.tag, + .pubkey(Keypair.test.publicKey.hex), + mentionedEventTag1.tag, + mentionedEventTag2.tag + ] + XCTAssertEqual(note.tags, expectedTags) + + try verifyEvent(note) + } + + func testCreateTextNoteThreadedReply() throws { + let noteToReply: TextNoteEvent = try decodeFixture(filename: "text_note") + + let relayURL = try XCTUnwrap(URL(string: "wss://relay.nostr.com")) + let mentionedEventTag1 = try XCTUnwrap(EventTag(eventId: "mentionednote1", relayURL: relayURL, marker: .mention)) + let mentionedEventTag2 = try XCTUnwrap(EventTag(eventId: "mentionednote2", relayURL: relayURL, marker: .mention)) + + let note = try XCTUnwrap( + TextNoteEvent.Builder() + .content("This is a reply to a note in a thread.") + .repliedEvent(noteToReply) + .mentionedEventTags([mentionedEventTag1, mentionedEventTag2]) + .build(signedBy: .test) + ) + + XCTAssertEqual(note.kind, .textNote) + XCTAssertEqual(note.content, "This is a reply to a note in a thread.") + XCTAssertEqual(note.pubkey, Keypair.test.publicKey.hex) + + let rootEventTag = try XCTUnwrap(noteToReply.rootEventTag) + let expectedRootEventTag = try XCTUnwrap(EventTag(eventId: rootEventTag.eventId, relayURL: rootEventTag.relayURL, marker: .root)) + let replyEventTag = try XCTUnwrap(EventTag(eventId: noteToReply.id, marker: .reply, pubkey: "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2")) + let expectedTags: [Tag] = [ + expectedRootEventTag.tag, + mentionedEventTag1.tag, + mentionedEventTag2.tag, + replyEventTag.tag, + .pubkey("f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9"), + .pubkey("82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2") + + ] + XCTAssertEqual(note.tags, expectedTags) try verifyEvent(note) } - func testCreateTextNoteReply() throws { + func testCreateTextNoteThreadedReplyDeprecated() throws { let noteToReply: TextNoteEvent = try decodeFixture(filename: "text_note") let relayURL = try XCTUnwrap(URL(string: "wss://relay.nostr.com")) let mentionedEventTag1 = try XCTUnwrap(EventTag(eventId: "mentionednote1", relayURL: relayURL, marker: .mention)) let mentionedEventTag2 = try XCTUnwrap(EventTag(eventId: "mentionednote2", relayURL: relayURL, marker: .mention)) - let note = try textNote(withContent: "This is a reply to a note in a thread.", replyingTo: noteToReply, mentionedEventTags: [mentionedEventTag1, mentionedEventTag2], signedBy: Keypair.test) + let note = try textNote(withContent: "This is a reply to a note in a thread.", replyingTo: noteToReply, mentionedEventTags: [mentionedEventTag1, mentionedEventTag2], signedBy: .test) XCTAssertEqual(note.kind, .textNote) XCTAssertEqual(note.content, "This is a reply to a note in a thread.") @@ -44,7 +208,7 @@ final class TextNoteEventTests: XCTestCase, EventCreating, EventVerifying, Fixtu let rootEventTag = try XCTUnwrap(noteToReply.rootEventTag) let expectedRootEventTag = try XCTUnwrap(EventTag(eventId: rootEventTag.eventId, relayURL: rootEventTag.relayURL, marker: .root)) - let replyEventTag = try XCTUnwrap(EventTag(eventId: noteToReply.id, marker: .reply)) + let replyEventTag = try XCTUnwrap(EventTag(eventId: noteToReply.id, marker: .reply, pubkey: "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2")) let expectedTags: [Tag] = [ expectedRootEventTag.tag, mentionedEventTag1.tag,