Skip to content

Commit

Permalink
Add missing fields to UserMetadataEvent and enable MetadataEvents to …
Browse files Browse the repository at this point in the history
…be updated without wiping out existing fields (nostr-sdk#165)
  • Loading branch information
tyiu authored and RandyMcMillan committed Sep 1, 2024
1 parent 9abb296 commit 24a0f9b
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 13 deletions.
60 changes: 49 additions & 11 deletions Sources/NostrSDK/Events/MetadataEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,69 @@
import Foundation

/// An object that describes a user.
/// > Note: [NIP-01 Specification](https://github.com/nostr-protocol/nips/blob/master/01.md#kinds)
public struct UserMetadata: Codable {

/// The user's name.
public let name: String?

/// The user's display name.
/// > Warning: This property is not part of the Nostr specifications.
/// > Note: [NIP-24 Extra metadata fields and tags](https://github.com/nostr-protocol/nips/blob/master/24.md#kind-0)
public let displayName: String?

/// The user's description of themself.
public let about: String?

/// The user's website address.
/// > Note: [NIP-24 Extra metadata fields and tags](https://github.com/nostr-protocol/nips/blob/master/24.md#kind-0)
public let website: URL?

/// The user's Nostr address.
///
/// > Note: [NIP-05 Specification](https://github.com/nostr-protocol/nips/blob/master/05.md#nip-05).
/// > Note: [NIP-05 Specification](https://github.com/nostr-protocol/nips/blob/master/05.md#finding-users-from-their-nip-05-identifier).
public let nostrAddress: String?

/// A URL to retrieve the user's picture.
public let pictureURL: URL?

/// A URL to retrieve the user's banner image.
/// > Note: [NIP-24 Extra metadata fields and tags](https://github.com/nostr-protocol/nips/blob/master/24.md#kind-0)
public let bannerPictureURL: URL?


/// A boolean to clarify that the content is entirely or partially the result of automation, such as with chatbots or newsfeeds.
/// > Note: [NIP-24 Extra metadata fields and tags](https://github.com/nostr-protocol/nips/blob/master/24.md#kind-0)
public let isBot: Bool?

/// The user's LUD-06 Lightning URL (LNURL).
/// > Note: [NIP-57 Lightning Zaps](https://github.com/nostr-protocol/nips/blob/master/57.md#protocol-flow)
public let lightningURLString: String?

/// The user's LUD-16 Lightning address.
/// > Note: [NIP-57 Lightning Zaps](https://github.com/nostr-protocol/nips/blob/master/57.md#protocol-flow)
public let lightningAddress: String?

enum CodingKeys: String, CodingKey {
case name, about, website
case nostrAddress = "nip05"
case pictureURL = "picture"
case bannerPictureURL = "banner"
case displayName = "display_name"
case isBot = "bot"
case lightningURLString = "lud06"
case lightningAddress = "lud16"
}

public init(name: String?, displayName: String?, about: String?, website: URL?, nostrAddress: String?, pictureURL: URL?, bannerPictureURL: URL?) {
public init(name: String? = nil, displayName: String? = nil, about: String? = nil, website: URL? = nil, nostrAddress: String? = nil, pictureURL: URL? = nil, bannerPictureURL: URL? = nil, isBot: Bool? = nil, lightningURLString: String? = nil, lightningAddress: String? = nil) {
self.name = name
self.displayName = displayName
self.about = about
self.website = website
self.nostrAddress = nostrAddress
self.pictureURL = pictureURL
self.bannerPictureURL = bannerPictureURL
self.isBot = isBot
self.lightningURLString = lightningURLString
self.lightningAddress = lightningAddress
}

public init(from decoder: Decoder) throws {
Expand All @@ -61,12 +82,15 @@ public struct UserMetadata: Codable {
nostrAddress = try container.decodeIfPresent(String.self, forKey: .nostrAddress)
pictureURL = try? container.decodeIfPresent(URL.self, forKey: .pictureURL)
bannerPictureURL = try? container.decodeIfPresent(URL.self, forKey: .bannerPictureURL)
isBot = try? container.decodeIfPresent(Bool.self, forKey: .isBot)
lightningURLString = try? container.decodeIfPresent(String.self, forKey: .lightningURLString)
lightningAddress = try? container.decodeIfPresent(String.self, forKey: .lightningAddress)
}
}

/// An event that contains a user profile.
///
/// > Note: [NIP-01 Specification](https://github.com/nostr-protocol/nips/blob/b503f8a92b22be3037b8115fe3e644865a4fa155/01.md#basic-event-kinds)
/// > Note: [NIP-01 Specification](https://github.com/nostr-protocol/nips/blob/master/01.md#kinds)
public final class MetadataEvent: NostrEvent, CustomEmojiInterpreting, NonParameterizedReplaceableEvent {

public required init(from decoder: Decoder) throws {
Expand Down Expand Up @@ -103,17 +127,31 @@ public final class MetadataEvent: NostrEvent, CustomEmojiInterpreting, NonParame
public extension EventCreating {

/// Creates a ``MetadataEvent`` (kind 0) and signs it with the provided ``Keypair``.
///
/// - Parameters:
/// - userMetadata: The metadata to set.
/// - userMetadata: The ``UserMetadata`` to set.
/// - rawUserMetadata: The dictionary of raw metadata to set that can contain fields unknown to any implemented NIPs.
/// - customEmojis: The custom emojis to emojify with if the matching shortcodes are found in the name or about fields.
/// - keypair: The Keypair to sign with.
/// - Returns: The signed ``MetadataEvent``.
///
/// See [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md)
func metadataEvent(withUserMetadata userMetadata: UserMetadata, customEmojis: [CustomEmoji] = [], signedBy keypair: Keypair) throws -> MetadataEvent {
let metadataAsData = try JSONEncoder().encode(userMetadata)
let metadataAsString = String(decoding: metadataAsData, as: UTF8.self)
/// > 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)
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)
}

let allUserMetadataAsString = String(decoding: allUserMetadataAsData, as: UTF8.self)
let customEmojiTags = customEmojis.map { $0.tag }
return try MetadataEvent(content: metadataAsString, tags: customEmojiTags, signedBy: keypair)
return try MetadataEvent(content: allUserMetadataAsString, tags: customEmojiTags, signedBy: keypair)
}
}
25 changes: 23 additions & 2 deletions Tests/NostrSDKTests/Events/MetadataEventTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,18 @@ final class MetadataEventTests: XCTestCase, EventCreating, EventVerifying, Fixtu
website: URL(string: "https://github.com/nostr-sdk/nostr-sdk-ios"),
nostrAddress: "[email protected]",
pictureURL: URL(string: "https://nostrsdk.com/picture.png"),
bannerPictureURL: URL(string: "https://nostrsdk.com/banner.png"))
bannerPictureURL: URL(string: "https://nostrsdk.com/banner.png"),
isBot: true,
lightningURLString: "LNURL1234567890",
lightningAddress: "[email protected]")

let rawUserMetadata: [String: Any] = [
"foo": "string",
"bool": true,
"number": 123,
"name": "This field should be ignored.",
"lud16": "[email protected]"
]

let ostrichImageURL = try XCTUnwrap(URL(string: "https://nostrsdk.com/ostrich.png"))
let appleImageURL = try XCTUnwrap(URL(string: "https://nostrsdk.com/apple.png"))
Expand All @@ -31,7 +42,7 @@ final class MetadataEventTests: XCTestCase, EventCreating, EventVerifying, Fixtu
Tag(name: .emoji, value: "apple", otherParameters: ["https://nostrsdk.com/apple.png"])
]

let event = try metadataEvent(withUserMetadata: meta, customEmojis: customEmojis, signedBy: Keypair.test)
let event = try metadataEvent(withUserMetadata: meta, rawUserMetadata: rawUserMetadata, customEmojis: customEmojis, signedBy: Keypair.test)
let expectedReplaceableEventCoordinates = try XCTUnwrap(EventCoordinates(kind: .metadata, pubkey: Keypair.test.publicKey))

XCTAssertEqual(event.userMetadata?.name, "Nostr SDK Test :ostrich:")
Expand All @@ -41,6 +52,14 @@ final class MetadataEventTests: XCTestCase, EventCreating, EventVerifying, Fixtu
XCTAssertEqual(event.userMetadata?.nostrAddress, "[email protected]")
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, "[email protected]")
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, "[email protected]")
XCTAssertEqual(event.customEmojis, customEmojis)
XCTAssertEqual(event.replaceableEventCoordinates(relayURL: nil), expectedReplaceableEventCoordinates)
XCTAssertEqual(event.tags, customEmojiTags)
Expand Down Expand Up @@ -68,6 +87,7 @@ final class MetadataEventTests: XCTestCase, EventCreating, EventVerifying, Fixtu
XCTAssertEqual(event.rawUserMetadata["nip05"] as? String, "[email protected]")
XCTAssertEqual(event.rawUserMetadata["picture"] as? String, "https://nostr.build/i/9396d5cd901304726883aea7363543f121e1d53964dd3149cadecd802608aebe.jpg")
XCTAssertEqual(event.rawUserMetadata["banner"] as? String, "https://nostr.build/i/nostr.build_90a51a2e50c9f42288260d01b3a2a4a1c7a9df085423abad7809e76429da7cdc.gif")
XCTAssertEqual(event.rawUserMetadata["lud16"] as? String, "[email protected]")

// access metadata properties from decoded object
let userMetadata = try XCTUnwrap(event.userMetadata)
Expand All @@ -77,6 +97,7 @@ final class MetadataEventTests: XCTestCase, EventCreating, EventVerifying, Fixtu
XCTAssertEqual(userMetadata.nostrAddress, "[email protected]")
XCTAssertEqual(userMetadata.pictureURL, URL(string: "https://nostr.build/i/9396d5cd901304726883aea7363543f121e1d53964dd3149cadecd802608aebe.jpg"))
XCTAssertEqual(userMetadata.bannerPictureURL, URL(string: "https://nostr.build/i/nostr.build_90a51a2e50c9f42288260d01b3a2a4a1c7a9df085423abad7809e76429da7cdc.gif"))
XCTAssertEqual(userMetadata.lightningAddress, "[email protected]")
}

func testDecodeMetadataWithEmptyWebsite() throws {
Expand Down

0 comments on commit 24a0f9b

Please sign in to comment.