diff --git a/Sources/NostrSDK/Events/ContactListEvent.swift b/Sources/NostrSDK/Events/ContactListEvent.swift new file mode 100644 index 0000000..c18f3f7 --- /dev/null +++ b/Sources/NostrSDK/Events/ContactListEvent.swift @@ -0,0 +1,60 @@ +// +// ContactListEvent.swift +// +// +// Created by Bryan Montz on 8/3/23. +// + +import Foundation + +/// Describes the permissions that a user has for a given relay. +public struct RelayPermissions: Equatable { + + /// Whether or not the user can read from the relay. + public let read: Bool + + /// Whether or not the user cn write to the relay. + public let write: Bool + + init(read: Bool, write: Bool) { + self.read = read + self.write = write + } + + init(dictionary: [AnyHashable: Any]) { + read = dictionary["read"] as? Bool ?? false + write = dictionary["write"] as? Bool ?? false + } +} + +/// A special event with kind 3, meaning "contact list" is defined as having a list of p tags, one for each of the followed/known profiles one is following. +/// +/// > Note: [NIP-02 Specification](https://github.com/nostr-protocol/nips/blob/master/02.md#contact-list-and-petnames) +public final class ContactListEvent: NostrEvent { + + /// Pubkeys for followed/known profiles. + var contactPubkeys: [String] { + tags.filter({ $0.name == .pubkey }).map { $0.value } + } + + /// Pubkey tags for followed/known profiles. + var contactPubkeyTags: [Tag] { + tags.filter({ $0.name == .pubkey }) + } + + /// Relays the user knows about. + /// + /// > Warning: This method of storing and accessing a user's relays is out of spec, not preferred, + /// and will be removed in the future. It is provided here for completeness and because of common usage. + @available(*, deprecated, message: "This method of storing and accessing a user's relays is out of spec, not preferred, and will be removed in the future.") + var relays: [String: RelayPermissions] { + guard let contentData = content.data(using: .utf8), + let contentDictionary = try? JSONSerialization.jsonObject(with: contentData) as? [String: [AnyHashable: Any]] else { + return [:] + } + return contentDictionary.reduce(into: [String: RelayPermissions]()) { partialResult, element in + let (key, value) = element + partialResult[key] = RelayPermissions(dictionary: value) + } + } +} diff --git a/Sources/NostrSDK/RelayResponse.swift b/Sources/NostrSDK/RelayResponse.swift index a99f7af..970d33a 100644 --- a/Sources/NostrSDK/RelayResponse.swift +++ b/Sources/NostrSDK/RelayResponse.swift @@ -21,6 +21,7 @@ fileprivate struct EventKindMapper: Decodable { // swiftlint:disable:this pr case .setMetadata: return SetMetadataEvent.self case .textNote: return TextNoteEvent.self case .recommendServer: return RecommendServerEvent.self + case .contactList: return ContactListEvent.self case .repost: return TextNoteRepostEvent.self case .genericRepost: return GenericRepostEvent.self default: return NostrEvent.self diff --git a/Tests/NostrSDKTests/EventDecodingTests.swift b/Tests/NostrSDKTests/EventDecodingTests.swift index b307e9e..3363400 100644 --- a/Tests/NostrSDKTests/EventDecodingTests.swift +++ b/Tests/NostrSDKTests/EventDecodingTests.swift @@ -78,7 +78,7 @@ final class EventDecodingTests: XCTestCase, FixtureLoading { func testDecodeContactList() throws { - let event: NostrEvent = try decodeFixture(filename: "contact_list") + let event: ContactListEvent = try decodeFixture(filename: "contact_list") XCTAssertEqual(event.id, "test-id") XCTAssertEqual(event.pubkey, "test-pubkey") @@ -93,6 +93,33 @@ final class EventDecodingTests: XCTestCase, FixtureLoading { XCTAssertEqual(event.signature, "hex-signature") } + func testDecodeContactListWithRelays() throws { + let event: ContactListEvent = try decodeFixture(filename: "contact_list_with_relays") + + let expectedPubkeys = [ + "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", + "07ecf9838136fe430fac43fa0860dbc62a0aac0729c5a33df1192ce75e330c9f", + "020f2d21ae09bf35fcdfb65decf1478b846f5f728ab30c5eaabcd6d081a81c3e", + "58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196", + "59fbee7369df7713dbbfa9bbdb0892c62eba929232615c6ff2787da384cb770f" + ] + + XCTAssertEqual(event.contactPubkeys, expectedPubkeys) + + let firstTag = Tag(name: .pubkey, value: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681") + XCTAssertEqual(event.contactPubkeyTags.first, firstTag) + + let expectedRelays = [ + "wss://relay.damus.io": RelayPermissions(read: true, write: true), + "wss://relay.current.fyi": RelayPermissions(read: false, write: true), + "wss://eden.nostr.land": RelayPermissions(read: true, write: true), + "wss://relay.snort.social": RelayPermissions(read: true, write: false), + "wss://nos.lol": RelayPermissions(read: true, write: true) + ] + + XCTAssertEqual(event.relays, expectedRelays) + } + func testDecodeRepost() throws { let event: TextNoteRepostEvent = try decodeFixture(filename: "repost") diff --git a/Tests/NostrSDKTests/Fixtures/contact_list_with_relays.json b/Tests/NostrSDKTests/Fixtures/contact_list_with_relays.json new file mode 100644 index 0000000..f04f35a --- /dev/null +++ b/Tests/NostrSDKTests/Fixtures/contact_list_with_relays.json @@ -0,0 +1,30 @@ +{ + "id": "367d16ca453b07552c3cd4cc553da62e5c6b20c59747417691b7fda4efdb055a", + "pubkey": "07ecf9838136fe430fac43fa0860dbc62a0aac0729c5a33df1192ce75e330c9f", + "created_at": 1681754187, + "kind": 3, + "tags": [ + [ + "p", + "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" + ], + [ + "p", + "07ecf9838136fe430fac43fa0860dbc62a0aac0729c5a33df1192ce75e330c9f" + ], + [ + "p", + "020f2d21ae09bf35fcdfb65decf1478b846f5f728ab30c5eaabcd6d081a81c3e" + ], + [ + "p", + "58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196" + ], + [ + "p", + "59fbee7369df7713dbbfa9bbdb0892c62eba929232615c6ff2787da384cb770f" + ] + ], + "content": "{\"wss://relay.damus.io\":{\"write\":true,\"read\":true},\"wss://relay.current.fyi\":{\"write\":true,\"read\":false},\"wss://eden.nostr.land\":{\"write\":true,\"read\":true},\"wss://relay.snort.social\":{\"write\":false,\"read\":true},\"wss://nos.lol\":{\"write\":true,\"read\":true}}", + "sig": "c83838d776595b6b7f31fb11970891402fc69983f88321a19c7f4f2b8891675b4c1eede4d73dc920068d353c90bbd839a34b4b03ebde34a3295c1c4b79094043" +}