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
  • Loading branch information
tyiu committed Jun 28, 2024
1 parent 0d47f6d commit df11749
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 14 deletions.
59 changes: 47 additions & 12 deletions Sources/NostrSDK/Events/MetadataEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,66 @@
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 bot: 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 lud06: 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 lud16: String?

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

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, bot: Bool? = nil, lud06: String? = nil, lud16: String? = nil) {
self.name = name
self.displayName = displayName
self.about = about
self.website = website
self.nostrAddress = nostrAddress
self.pictureURL = pictureURL
self.bannerPictureURL = bannerPictureURL
self.bot = bot
self.lud06 = lud06
self.lud16 = lud16
}

public init(from decoder: Decoder) throws {
Expand All @@ -61,12 +79,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)
bot = try? container.decodeIfPresent(Bool.self, forKey: .bot)
lud06 = try? container.decodeIfPresent(String.self, forKey: .lud06)
lud16 = try? container.decodeIfPresent(String.self, forKey: .lud16)
}
}

/// 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 +124,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)
}
}
20 changes: 18 additions & 2 deletions Tests/NostrSDKTests/Events/MetadataEventTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,17 @@ 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"),
bot: true,
lud06: "LNURL1234567890",
lud16: "[email protected]")

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

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 +41,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 +51,12 @@ 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?.bot, true)
XCTAssertEqual(event.userMetadata?.lud06, "LNURL1234567890")
XCTAssertEqual(event.userMetadata?.lud16, "[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.customEmojis, customEmojis)
XCTAssertEqual(event.replaceableEventCoordinates(relayURL: nil), expectedReplaceableEventCoordinates)
XCTAssertEqual(event.tags, customEmojiTags)
Expand Down

0 comments on commit df11749

Please sign in to comment.