diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c49e093..f0949d1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,8 +2,7 @@ name: Docs on: push: - branches: - - main + branches: [ "main" ] workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml index 526a758..9420d50 100644 --- a/.github/workflows/swiftlint.yml +++ b/.github/workflows/swiftlint.yml @@ -2,9 +2,9 @@ name: SwiftLint on: push: - branches: [ '**' ] - pull_request_target: - branches: [ '**' ] + branches: [ "main" ] + pull_request: + types: [opened, synchronize, reopened] workflow_dispatch: jobs: diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 3faf441..6b91e79 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -2,9 +2,9 @@ name: Unit Tests on: push: - branches: [ '**' ] - pull_request_target: - branches: [ '**' ] + branches: [ "main" ] + pull_request: + types: [opened, synchronize, reopened] workflow_dispatch: jobs: @@ -12,7 +12,7 @@ jobs: runs-on: macos-latest strategy: matrix: - swift: ['5.8', '5.9', '5.10'] + swift: ['5.8', '5.9', '5.10', '6.0'] steps: - name: Checkout diff --git a/Sources/NostrSDK/EventKind.swift b/Sources/NostrSDK/EventKind.swift index 5175501..bd99263 100644 --- a/Sources/NostrSDK/EventKind.swift +++ b/Sources/NostrSDK/EventKind.swift @@ -69,6 +69,26 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha /// See [NIP-18](https://github.com/nostr-protocol/nips/blob/master/18.md#nip-18). case genericRepost + /// Create a public chat channel. + /// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-40-create-channel). + case channelCreation + + /// Update a channel's public metadata. + /// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-41-set-channel-metadata). + case channelMetadata + + /// Send a text message to a channel. + /// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-42-create-channel-message). + case channelMessage + + /// User no longer wants to see a certain message. + /// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-43-hide-message). + case channelHideMessage + + /// User no longer wants to see messages from another user. + /// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-44-mute-user). + case channelMuteUser + /// This kind of event wraps a `seal` event. /// The wrapped seal is always encrypted to a receiver's pubkey using a random, one-time-use private key. /// The gift wrap event tags should include any information needed to route the event to its intended recipient, @@ -140,6 +160,11 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha .reaction, .seal, .genericRepost, + .channelCreation, + .channelMetadata, + .channelMessage, + .channelHideMessage, + .channelMuteUser, .giftWrap, .report, .muteList, @@ -173,6 +198,11 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha case .reaction: return 7 case .seal: return 13 case .genericRepost: return 16 + case .channelCreation: return 40 + case .channelMetadata: return 41 + case .channelMessage: return 42 + case .channelHideMessage: return 43 + case .channelMuteUser: return 44 case .giftWrap: return 1059 case .report: return 1984 case .muteList: return 10000 @@ -200,6 +230,11 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha case .reaction: return ReactionEvent.self case .seal: return SealEvent.self case .genericRepost: return GenericRepostEvent.self + case .channelCreation: return CreateChannelEvent.self + case .channelMetadata: return SetChannelMetadataEvent.self + case .channelMessage: return CreateChannelMessageEvent.self + case .channelHideMessage: return HideChannelMessageEvent.self + case .channelMuteUser: return MuteChannelUserEvent.self case .giftWrap: return GiftWrapEvent.self case .report: return ReportEvent.self case .muteList: return MuteListEvent.self diff --git a/Sources/NostrSDK/EventSerializer.swift b/Sources/NostrSDK/EventSerializer.swift index 0917ddb..d275d5b 100644 --- a/Sources/NostrSDK/EventSerializer.swift +++ b/Sources/NostrSDK/EventSerializer.swift @@ -31,14 +31,14 @@ public enum EventSerializer { let tagsString: String if let tagsData = try? encoder.encode(tags) { - tagsString = String(decoding: tagsData, as: UTF8.self) + tagsString = String(data: tagsData, encoding: .utf8) ?? "[]" } else { tagsString = "[]" } let contentString: String if let contentData = try? encoder.encode(content) { - contentString = String(decoding: contentData, as: UTF8.self) + contentString = String(data: contentData, encoding: .utf8) ?? "\"\"" } else { contentString = "\"\"" } diff --git a/Sources/NostrSDK/Events/BookmarksListEvent.swift b/Sources/NostrSDK/Events/BookmarksListEvent.swift index d5ae1d9..64e968a 100644 --- a/Sources/NostrSDK/Events/BookmarksListEvent.swift +++ b/Sources/NostrSDK/Events/BookmarksListEvent.swift @@ -149,8 +149,8 @@ public extension EventCreating { var encryptedContent: String? if !privateTags.isEmpty { let rawPrivateTags = privateTags.map { $0.raw } - if let unencryptedData = try? JSONSerialization.data(withJSONObject: rawPrivateTags) { - let unencryptedContent = String(decoding: unencryptedData, as: UTF8.self) + if let unencryptedData = try? JSONSerialization.data(withJSONObject: rawPrivateTags), + let unencryptedContent = String(data: unencryptedData, encoding: .utf8) { encryptedContent = try legacyEncrypt(content: unencryptedContent, privateKey: keypair.privateKey, publicKey: keypair.publicKey) diff --git a/Sources/NostrSDK/Events/Channels/ChannelMetadata.swift b/Sources/NostrSDK/Events/Channels/ChannelMetadata.swift new file mode 100644 index 0000000..68202a6 --- /dev/null +++ b/Sources/NostrSDK/Events/Channels/ChannelMetadata.swift @@ -0,0 +1,45 @@ +// +// ChannelMetadata.swift +// +// +// Created by Konstantin Yurchenko, Jr on 9/20/24. +// + +import Foundation + +/// A structure that describes channel. +/// +/// See [NIP-28 Specification](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-40-create-channel). +public struct ChannelMetadata: Codable { + /// Channel name + public let name: String? + /// Channel desctription + public let about: String? + /// URL of channel picture + public let picture: String? + /// List of relays to download and broadcast events to + public let relays: [String]? + + enum CodingKeys: String, CodingKey { + case name + case about + case picture + case relays + } + + public init(name: String? = nil, about: String? = nil, picture: String? = nil, relays: [String] = []) { + self.name = name + self.about = about + self.picture = picture + self.relays = relays + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + name = try container.decodeIfPresent(String.self, forKey: .name) + about = try container.decodeIfPresent(String.self, forKey: .about) + picture = try container.decodeIfPresent(String.self, forKey: .picture) + relays = try container.decodeIfPresent([String].self, forKey: .relays) + } +} diff --git a/Sources/NostrSDK/Events/Channels/CreateChannelEvent.swift b/Sources/NostrSDK/Events/Channels/CreateChannelEvent.swift new file mode 100644 index 0000000..c0b3dc4 --- /dev/null +++ b/Sources/NostrSDK/Events/Channels/CreateChannelEvent.swift @@ -0,0 +1,76 @@ +// +// CreateChannelEvent.swift +// +// +// Created by Konstantin Yurchenko, Jr on 9/11/24. +// + +import Foundation + +/// Create a public chat channel. +/// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-40-create-channel). +public class CreateChannelEvent: NostrEvent { + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + } + + @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(*, 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) + } + + class var kind: EventKind { + .channelCreation + } +} + +public extension EventCreating { + func createChannelEvent(withContent content: String, signedBy keypair: Keypair) throws -> CreateChannelEvent { + return try CreateChannelEvent(content: content, tags: [], signedBy: keypair) + } +} + +public extension CreateChannelEvent { + /// Builder of ``CreateChannelEvent``. + final class Builder: NostrEvent.Builder { + public init() { + super.init(kind: .channelCreation) + } + + public final func channelMetadata(_ channelMetadata: ChannelMetadata, merging rawChannelMetadata: [String: Any] = [:]) throws -> Self { + let channelMetadataAsData = try JSONEncoder().encode(channelMetadata) + + let allChannelMetadataAsData: Data + if rawChannelMetadata.isEmpty { + allChannelMetadataAsData = channelMetadataAsData + } else { + var channelMetadataAsDictionary = try JSONSerialization.jsonObject(with: channelMetadataAsData, options: []) as? [String: Any] ?? [:] + channelMetadataAsDictionary.merge(rawChannelMetadata) { (current, _) in current } + allChannelMetadataAsData = try JSONSerialization.data(withJSONObject: channelMetadataAsDictionary, options: .sortedKeys) + } + + guard let allChannelMetadataAsString = String(data: allChannelMetadataAsData, encoding: .utf8) else { + throw EventCreatingError.invalidInput + } + + content(allChannelMetadataAsString) + + return self + } + } +} diff --git a/Sources/NostrSDK/Events/Channels/CreateChannelMessageEvent.swift b/Sources/NostrSDK/Events/Channels/CreateChannelMessageEvent.swift new file mode 100644 index 0000000..d66cef1 --- /dev/null +++ b/Sources/NostrSDK/Events/Channels/CreateChannelMessageEvent.swift @@ -0,0 +1,52 @@ +// +// CreateChannelMessageEvent.swift +// +// +// Created by Konstantin Yurchenko, Jr on 9/11/24. +// + +import Foundation + +/// Send a text message to a channel. +/// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-42-create-channel-message). +public class CreateChannelMessageEvent: NostrEvent { + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + } + + @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(*, 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) + } + + class var kind: EventKind { + .channelMessage + } +} + +public extension EventCreating { + func createChannelMessageEvent(withContent content: String, eventId: String, relayUrl: String, signedBy keypair: Keypair) throws -> CreateChannelMessageEvent { + + var tags: [Tag] = [ + Tag.pubkey(keypair.publicKey.hex), + Tag.event(eventId, otherParameters: [relayUrl, "root"]), + ] + + return try CreateChannelMessageEvent(content: content, tags: tags, signedBy: keypair) + } +} diff --git a/Sources/NostrSDK/Events/Channels/HideChannelMessageEvent.swift b/Sources/NostrSDK/Events/Channels/HideChannelMessageEvent.swift new file mode 100644 index 0000000..0146a95 --- /dev/null +++ b/Sources/NostrSDK/Events/Channels/HideChannelMessageEvent.swift @@ -0,0 +1,46 @@ +// +// HideChannelMessageEvent.swift +// +// +// Created by Konstantin Yurchenko, Jr on 9/11/24. +// + +import Foundation + +/// User no longer wants to see a certain message. +/// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-43-hide-message). +public class HideChannelMessageEvent: NostrEvent { + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + } + + @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(*, 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) + } + + class var kind: EventKind { + .channelHideMessage + } +} + +public extension EventCreating { + func hideChannelMessageEvent(withContent content: String, signedBy keypair: Keypair) throws -> HideChannelMessageEvent { + return try HideChannelMessageEvent(content: content, tags: [], signedBy: keypair) + } +} diff --git a/Sources/NostrSDK/Events/Channels/MuteChannelUserEvent.swift b/Sources/NostrSDK/Events/Channels/MuteChannelUserEvent.swift new file mode 100644 index 0000000..9d8eb12 --- /dev/null +++ b/Sources/NostrSDK/Events/Channels/MuteChannelUserEvent.swift @@ -0,0 +1,46 @@ +// +// MuteChannelUserEvent.swift +// +// +// Created by Konstantin Yurchenko, Jr on 9/11/24. +// + +import Foundation + +/// User no longer wants to see messages from another user. +/// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-44-mute-user). +public class MuteChannelUserEvent: NostrEvent { + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + } + + @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(*, 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) + } + + class var kind: EventKind { + .channelMuteUser + } +} + +public extension EventCreating { + func createMuteChannelUserEvent(withContent content: String, signedBy keypair: Keypair) throws -> MuteChannelUserEvent { + return try MuteChannelUserEvent(content: content, tags: [], signedBy: keypair) + } +} diff --git a/Sources/NostrSDK/Events/Channels/SetChannelMetadataEvent.swift b/Sources/NostrSDK/Events/Channels/SetChannelMetadataEvent.swift new file mode 100644 index 0000000..8da5235 --- /dev/null +++ b/Sources/NostrSDK/Events/Channels/SetChannelMetadataEvent.swift @@ -0,0 +1,46 @@ +// +// SetChannelMetadataEvent.swift +// +// +// Created by Konstantin Yurchenko, Jr on 9/11/24. +// + +import Foundation + +/// Update a channel's public metadata. +/// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-41-set-channel-metadata). +public class SetChannelMetadataEvent: NostrEvent { + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + } + + @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(*, 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) + } + + class var kind: EventKind { + .channelMetadata + } +} + +public extension EventCreating { + func setChannelMetadataEvent(withContent content: String, signedBy keypair: Keypair) throws -> SetChannelMetadataEvent { + return try SetChannelMetadataEvent(content: content, tags: [], signedBy: keypair) + } +} diff --git a/Sources/NostrSDK/Events/GenericRepostEvent.swift b/Sources/NostrSDK/Events/GenericRepostEvent.swift index b2f2ac5..858598b 100644 --- a/Sources/NostrSDK/Events/GenericRepostEvent.swift +++ b/Sources/NostrSDK/Events/GenericRepostEvent.swift @@ -80,7 +80,9 @@ public extension EventCreating { /// See [NIP-18](https://github.com/nostr-protocol/nips/blob/master/18.md#reposts). func repost(event: NostrEvent, signedBy keypair: Keypair) throws -> GenericRepostEvent { let jsonData = try JSONEncoder().encode(event) - let stringifiedJSON = String(decoding: jsonData, as: UTF8.self) + guard let stringifiedJSON = String(data: jsonData, encoding: .utf8) else { + throw EventCreatingError.invalidInput + } var tags: [Tag] = [ .event(event.id), .pubkey(event.pubkey) diff --git a/Sources/NostrSDK/Events/GiftWrap/GiftWrapEvent.swift b/Sources/NostrSDK/Events/GiftWrap/GiftWrapEvent.swift index c11729c..c05b75f 100644 --- a/Sources/NostrSDK/Events/GiftWrap/GiftWrapEvent.swift +++ b/Sources/NostrSDK/Events/GiftWrap/GiftWrapEvent.swift @@ -119,7 +119,9 @@ public extension EventCreating { signedBy keypair: Keypair ) throws -> GiftWrapEvent { let jsonData = try JSONEncoder().encode(seal) - let stringifiedJSON = String(decoding: jsonData, as: UTF8.self) + guard let stringifiedJSON = String(data: jsonData, encoding: .utf8) else { + throw GiftWrapError.utf8EncodingFailed + } guard let randomKeypair = Keypair() else { throw GiftWrapError.keypairGenerationFailed diff --git a/Sources/NostrSDK/Events/GiftWrap/SealEvent.swift b/Sources/NostrSDK/Events/GiftWrap/SealEvent.swift index 3b1ebba..44cea59 100644 --- a/Sources/NostrSDK/Events/GiftWrap/SealEvent.swift +++ b/Sources/NostrSDK/Events/GiftWrap/SealEvent.swift @@ -96,7 +96,10 @@ public extension EventCreating { } let jsonData = try JSONEncoder().encode(rumor) - let stringifiedJSON = String(decoding: jsonData, as: UTF8.self) + guard let stringifiedJSON = String(data: jsonData, encoding: .utf8) else { + throw SealEventError.utf8EncodingFailed + } + let encryptedRumor = try encrypt(plaintext: stringifiedJSON, privateKeyA: keypair.privateKey, publicKeyB: recipient) return try SealEvent(content: encryptedRumor, createdAt: createdAt, signedBy: keypair) } diff --git a/Sources/NostrSDK/Events/MetadataEvent.swift b/Sources/NostrSDK/Events/MetadataEvent.swift index c5f2e85..c57fab2 100644 --- a/Sources/NostrSDK/Events/MetadataEvent.swift +++ b/Sources/NostrSDK/Events/MetadataEvent.swift @@ -184,7 +184,10 @@ public extension MetadataEvent { allUserMetadataAsData = try JSONSerialization.data(withJSONObject: userMetadataAsDictionary, options: .sortedKeys) } - let allUserMetadataAsString = String(decoding: allUserMetadataAsData, as: UTF8.self) + guard let allUserMetadataAsString = String(data: allUserMetadataAsData, encoding: .utf8) else { + throw EventCreatingError.invalidInput + } + content(allUserMetadataAsString) return self diff --git a/Sources/NostrSDK/Events/MuteListEvent.swift b/Sources/NostrSDK/Events/MuteListEvent.swift index d70f37b..1637509 100644 --- a/Sources/NostrSDK/Events/MuteListEvent.swift +++ b/Sources/NostrSDK/Events/MuteListEvent.swift @@ -108,8 +108,8 @@ public extension EventCreating { var encryptedContent: String? if !privateTags.isEmpty { let rawPrivateTags = privateTags.map { $0.raw } - if let unencryptedData = try? JSONSerialization.data(withJSONObject: rawPrivateTags) { - let unencryptedContent = String(decoding: unencryptedData, as: UTF8.self) + if let unencryptedData = try? JSONSerialization.data(withJSONObject: rawPrivateTags), + let unencryptedContent = String(data: unencryptedData, encoding: .utf8) { encryptedContent = try legacyEncrypt(content: unencryptedContent, privateKey: keypair.privateKey, publicKey: keypair.publicKey) diff --git a/Sources/NostrSDK/LegacyDirectMessageEncrypting.swift b/Sources/NostrSDK/LegacyDirectMessageEncrypting.swift index e11feb9..8288197 100644 --- a/Sources/NostrSDK/LegacyDirectMessageEncrypting.swift +++ b/Sources/NostrSDK/LegacyDirectMessageEncrypting.swift @@ -77,11 +77,12 @@ public extension LegacyDirectMessageEncrypting { let ivContentTrimmed = ivContent.dropFirst(3) guard let ivContentData = Data(base64Encoded: String(ivContentTrimmed)), - let decryptedContentData = AESDecrypt(data: encryptedContentData.bytes, iv: ivContentData.bytes, sharedSecret: sharedSecret) else { + let decryptedContentData = AESDecrypt(data: encryptedContentData.bytes, iv: ivContentData.bytes, sharedSecret: sharedSecret), + let decodedContent = String(data: decryptedContentData, encoding: .utf8) else { throw LegacyDirectMessageEncryptingError.decryptionError } - return String(decoding: decryptedContentData, as: UTF8.self) + return decodedContent } private func getSharedSecret(privateKey: PrivateKey, recipient pubkey: PublicKey) throws -> [UInt8] { diff --git a/Sources/NostrSDK/NIP44v2Encrypting.swift b/Sources/NostrSDK/NIP44v2Encrypting.swift index bacbbac..fea1a2a 100644 --- a/Sources/NostrSDK/NIP44v2Encrypting.swift +++ b/Sources/NostrSDK/NIP44v2Encrypting.swift @@ -149,12 +149,12 @@ extension NIP44v2Encrypting { guard unpaddedLength > 0, unpadded.count == unpaddedLength, - padded.count == 2 + paddedLength - else { + padded.count == 2 + paddedLength, + let result = String(data: Data(unpadded), encoding: .utf8) else { throw NIP44v2EncryptingError.paddingInvalid } - return String(decoding: Data(unpadded), as: UTF8.self) + return result } func decodePayload(_ payload: String) throws -> DecodedPayload { diff --git a/Sources/NostrSDK/RelayRequest.swift b/Sources/NostrSDK/RelayRequest.swift index a9e9961..5626170 100644 --- a/Sources/NostrSDK/RelayRequest.swift +++ b/Sources/NostrSDK/RelayRequest.swift @@ -29,9 +29,10 @@ enum RelayRequest { payload = [AnyEncodable("COUNT"), AnyEncodable(subscriptionId), AnyEncodable(filter)] } - guard let data = try? JSONEncoder().encode(payload) else { + guard let data = try? JSONEncoder().encode(payload), + let decoded = String(data: data, encoding: .utf8) else { return nil } - return String(decoding: data, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) + return decoded.trimmingCharacters(in: .whitespacesAndNewlines) } } diff --git a/Sources/NostrSDK/WebSocket.swift b/Sources/NostrSDK/WebSocket.swift index 91ddf1c..bbd1bfb 100644 --- a/Sources/NostrSDK/WebSocket.swift +++ b/Sources/NostrSDK/WebSocket.swift @@ -80,7 +80,7 @@ final class WebSocket: NSObject, URLSessionWebSocketDelegate { let reasonString: String? if let reason { - reasonString = String(decoding: reason, as: UTF8.self) + reasonString = String(data: reason, encoding: .utf8) } else { reasonString = nil } diff --git a/Tests/NostrSDKTests/FilterTests.swift b/Tests/NostrSDKTests/FilterTests.swift index 951db47..558211e 100644 --- a/Tests/NostrSDKTests/FilterTests.swift +++ b/Tests/NostrSDKTests/FilterTests.swift @@ -19,7 +19,7 @@ final class FilterTests: XCTestCase, FixtureLoading, JSONTesting { let encoder = JSONEncoder() let result = try encoder.encode(filter) - let resultString = String(decoding: result, as: UTF8.self) + let resultString = try XCTUnwrap(String(data: result, encoding: .utf8)) XCTAssertTrue(areEquivalentJSONObjectStrings(expected, resultString)) } @@ -39,7 +39,7 @@ final class FilterTests: XCTestCase, FixtureLoading, JSONTesting { let encoder = JSONEncoder() let result = try encoder.encode(filter) - let resultString = String(decoding: result, as: UTF8.self) + let resultString = try XCTUnwrap(String(data: result, encoding: .utf8)) XCTAssertTrue(areEquivalentJSONObjectStrings(expected, resultString)) } diff --git a/Tests/NostrSDKTests/FixtureLoading.swift b/Tests/NostrSDKTests/FixtureLoading.swift index 8e9837b..4d4a76e 100644 --- a/Tests/NostrSDKTests/FixtureLoading.swift +++ b/Tests/NostrSDKTests/FixtureLoading.swift @@ -27,7 +27,11 @@ extension FixtureLoading { func loadFixtureString(_ filename: String) throws -> String? { let data = try loadFixtureData(filename) - let originalString = String(decoding: data, as: UTF8.self) + + guard let originalString = String(data: data, encoding: .utf8) else { + throw FixtureLoadingError.decodingError + } + let trimmedString = originalString.filter { !"\n\t\r".contains($0) } return trimmedString }