Skip to content

Commit

Permalink
Add support for NIP-65 Relay List Metadata (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
tyiu authored Jul 15, 2024
1 parent cfa87e7 commit 4a9ff87
Show file tree
Hide file tree
Showing 9 changed files with 397 additions and 5 deletions.
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
"version" : "0.12.2"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d",
"version" : "1.1.2"
}
},
{
"identity" : "swift-docc-plugin",
"kind" : "remoteSourceControl",
Expand Down
6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ let package = Package(
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
.package(url: "https://github.com/GigaBitcoin/secp256k1.swift", from: "0.12.2"),
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.8.1"))
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.8.1")),
.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.1.2"))
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand All @@ -26,7 +27,8 @@ let package = Package(
name: "NostrSDK",
dependencies: [
.product(name: "secp256k1", package: "secp256k1.swift"),
"CryptoSwift"
"CryptoSwift",
.product(name: "OrderedCollections", package: "swift-collections")
]
),
.testTarget(
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ The following [NIPs](https://github.com/nostr-protocol/nips) are implemented:
- [ ] [NIP-57: Lightning Zaps](https://github.com/nostr-protocol/nips/blob/master/57.md)
- [ ] [NIP-58: Badges](https://github.com/nostr-protocol/nips/blob/master/58.md)
- [x] [NIP-59: Gift Wrap](https://github.com/nostr-protocol/nips/blob/master/59.md)
- [ ] [NIP-65: Relay List Metadata](https://github.com/nostr-protocol/nips/blob/master/65.md)
- [x] [NIP-65: Relay List Metadata](https://github.com/nostr-protocol/nips/blob/master/65.md)
- [ ] [NIP-72: Moderated Communities](https://github.com/nostr-protocol/nips/blob/master/72.md)
- [ ] [NIP-75: Zap Goals](https://github.com/nostr-protocol/nips/blob/master/75.md)
- [ ] [NIP-78: Application-specific data](https://github.com/nostr-protocol/nips/blob/master/78.md)
Expand Down
10 changes: 9 additions & 1 deletion Sources/NostrSDK/EventKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha
///
/// See [NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md#standard-lists)
case muteList


/// This kind of replaceable event advertises preferred relays for discovering a user's content and receiving fresh content from others.
///
/// See [NIP-65 - Relay List Metadata](https://github.com/nostr-protocol/nips/blob/master/65.md)
case relayListMetadata

/// This kind of event contains an uncategorized, "global" list of things a user wants to save.
///
/// See [NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md#standard-lists)
Expand Down Expand Up @@ -138,6 +143,7 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha
.giftWrap,
.report,
.muteList,
.relayListMetadata,
.bookmarksList,
.authentication,
.longformContent,
Expand Down Expand Up @@ -170,6 +176,7 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha
case .giftWrap: return 1059
case .report: return 1984
case .muteList: return 10000
case .relayListMetadata: return 10002
case .bookmarksList: return 10003
case .authentication: return 22242
case .longformContent: return 30023
Expand All @@ -196,6 +203,7 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha
case .giftWrap: return GiftWrapEvent.self
case .report: return ReportEvent.self
case .muteList: return MuteListEvent.self
case .relayListMetadata: return RelayListMetadataEvent.self
case .bookmarksList: return BookmarksListEvent.self
case .authentication: return AuthenticationEvent.self
case .longformContent: return LongformContentEvent.self
Expand Down
158 changes: 158 additions & 0 deletions Sources/NostrSDK/Events/RelayListMetadataEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//
// RelayListMetadataEvent.swift
//
//
// Created by Terry Yiu on 7/13/24.
//

import Foundation
import OrderedCollections

/// Defines a replaceable event using kind 10002 to advertise preferred relays for discovering a user's content and receiving fresh content from others.
/// This event doesn't fully replace relay lists that are designed to configure a client's usage of relays.
/// Clients MAY use other relay lists in situations where ``RelayListMetadataEvent`` cannot be found.
///
/// When seeking events from a user, clients SHOULD use the WRITE relays.
/// When seeking events about a user, where the user was tagged, clients SHOULD use the READ relays.
///
/// When broadcasting an event, clients SHOULD:
/// - Broadcast the event to the WRITE relays of the author
/// - Broadcast the event to all READ relays of each tagged user
///
/// > Note: [NIP 65 - Relay List Metadata](https://github.com/nostr-protocol/nips/blob/master/65.md)
public final class RelayListMetadataEvent: NostrEvent, NonParameterizedReplaceableEvent {

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 {
try super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, signedBy: keypair)
}

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)
}

/// The list of ``UserRelayMetadata`` that describes preferred relays for discovering the user's content and receiving fresh content from others
public var relayMetadataList: [UserRelayMetadata] {
tags.compactMap { UserRelayMetadata(tag: $0) }
}
}

/// Describes a preferred relay for discovering a user's content and receiving fresh content from others.
public struct UserRelayMetadata: Equatable {
/// The URL of the preferred relay.
public let relayURL: URL

/// The relay marker describing what type of events might be found from the preferred relay.
public let marker: Marker

public enum Marker {
/// When seeking events about the user who authored the ``RelayListMetadataEvent``, where the user was tagged,
/// clients SHOULD use this relay as a read relay.
case read

/// When seeking events from the user who authored the ``RelayListMetadataEvent``,
/// clients SHOULD use this relay as a write relay.
case write

/// When seeking events about the user who authored the ``RelayListMetadataEvent``, where the user was tagged,
/// or when seeking events from the user who authored the ``RelayListMetadataEvent``,
/// clients SHOULD use this relay as a read and write relay.
case readAndWrite
}

/// Creates a ``UserRelayMetadata`` from a ``Tag``.
/// The tag must have a tag name of `r`, value of a valid relay URL string, and, optionally, a marker of `read` or `write`.
/// If the marker is omitted, the relay is used for both read and write.
///
/// A `nil` value is returned if the relay URL string is invalid or the marker is invalid.
///
/// > Note: [NIP 65 - Relay List Metadata](https://github.com/nostr-protocol/nips/blob/master/65.md)
public init?(tag: Tag) {
guard tag.name == "r", let relayURL = try? RelayURLValidator.shared.validateRelayURLString(tag.value) else {
return nil
}

switch tag.otherParameters.first {
case "read":
marker = .read
case "write":
marker = .write
case .none:
marker = .readAndWrite
case .some:
return nil
}

self.relayURL = relayURL
}

/// Creates a ``UserRelayMetadata`` from a relay ``URL`` and ``Marker``.
///
/// A `nil` value is returned if the relay URL string is invalid.
///
/// > Note: [NIP 65 - Relay List Metadata](https://github.com/nostr-protocol/nips/blob/master/65.md)
public init?(relayURL: URL, marker: Marker = .readAndWrite) {
guard let validatedRelayURL = try? RelayURLValidator.shared.validateRelayURL(relayURL) else {
return nil
}
self.relayURL = validatedRelayURL
self.marker = marker
}

/// The ``Tag`` that represents the user relay metadata that can be used in a ``RelayListMetadataEvent``.
///
/// Note that if this ``UserRelayMetadata`` was initialized with a ``Tag``, the tag returned by this property
/// may not be the same as the original tag.
/// For example, if there are extra parameters in the original tag that is not recognized by [NIP-65](https://github.com/nostr-protocol/nips/blob/master/65.md),
/// they will not be returned by this property.
public var tag: Tag {
let otherParameters: [String]
switch marker {
case .read:
otherParameters = ["read"]
case .write:
otherParameters = ["write"]
case .readAndWrite:
otherParameters = []
}
return Tag(name: "r", value: relayURL.absoluteString, otherParameters: otherParameters)
}
}

public extension EventCreating {
/// Creates a ``RelayListMetadataEvent`` (kind 10002).
/// - Parameters:
/// - relayMetadataList: The list of ``UserRelayMetadata``.
/// - keypair: The ``Keypair`` to sign the event with.
func relayListMetadataEvent(withRelayMetadataList relayMetadataList: [UserRelayMetadata], signedBy keypair: Keypair) throws -> RelayListMetadataEvent {
// Using an ordered dictionary to retain the order of the list while de-duplicating the data.
var deduplicatedMetadata = OrderedDictionary<URL, UserRelayMetadata>()

for metadata in relayMetadataList {
if let existingMetadata = deduplicatedMetadata[metadata.relayURL] {
// If the user relay metadata marker is identical between the duplicates,
// or if the existing one already has a read and write marker, skip it.
if existingMetadata.marker == metadata.marker || existingMetadata.marker == .readAndWrite {
continue
}

// Any other permutation of markers will result in a combined marker of read and write.
if metadata.marker == .readAndWrite {
// If the marker on `metadata` is set to read and write, just use that as the value
// instead of creating a new object (as a micro-optimization).
deduplicatedMetadata[metadata.relayURL] = metadata
} else {
deduplicatedMetadata[metadata.relayURL] = UserRelayMetadata(relayURL: metadata.relayURL, marker: .readAndWrite)
}
} else {
deduplicatedMetadata[metadata.relayURL] = metadata
}
}

return try RelayListMetadataEvent(tags: deduplicatedMetadata.map { $0.value.tag }, signedBy: keypair)
}
}
2 changes: 1 addition & 1 deletion Sources/NostrSDK/Tag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,6 @@ extension Tag {

extension Tag: CustomDebugStringConvertible {
public var debugDescription: String {
"Tag(name: \"\(name)\", value: \"\(value)\")"
"Tag(name: \"\(name)\", value: \"\(value)\", otherParameters: \(otherParameters)"
}
}
Loading

0 comments on commit 4a9ff87

Please sign in to comment.