NIP-65 relay list models and definitions
This commit adds the base models needed for the NIP-65 relay list support. This introduces no user-facing changes. Changelog-None Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -1649,6 +1649,9 @@
|
||||
D7DB93052D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||
D7DB93062D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||
D7DB93072D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||
D7DB930A2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
|
||||
D7DB930B2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
|
||||
D7DB930C2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
|
||||
D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
|
||||
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
|
||||
D7EB00B02CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
|
||||
@@ -2537,6 +2540,7 @@
|
||||
D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = nip44.vectors.json; sourceTree = "<group>"; };
|
||||
D7DB1FF22D5AC5E400CF06DA /* LICENSES */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSES; sourceTree = "<group>"; };
|
||||
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Undistractor.swift; sourceTree = "<group>"; };
|
||||
D7DB93092D69485A00DA1EE5 /* NIP65.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP65.swift; sourceTree = "<group>"; };
|
||||
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
|
||||
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentFullScreenItemNotify.swift; sourceTree = "<group>"; };
|
||||
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
|
||||
@@ -3646,6 +3650,7 @@
|
||||
4CE6DEE527F7A08100C66700 /* damus */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7DB93082D69478400DA1EE5 /* NIP65 */,
|
||||
D7DB1FDC2D5A77E500CF06DA /* NIP44 */,
|
||||
D755B28B2D3E7D6500BBEEFA /* NIP37 */,
|
||||
D78F08152D7F7F5F00FC6C75 /* NIP04 */,
|
||||
@@ -4044,6 +4049,14 @@
|
||||
path = NIP44;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D7DB93082D69478400DA1EE5 /* NIP65 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7DB93092D69485A00DA1EE5 /* NIP65.swift */,
|
||||
);
|
||||
path = NIP65;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E06336A72B7582D600A88E6B /* Assets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -4480,6 +4493,7 @@
|
||||
F757933A29D7AECD007DEAC1 /* MediaPicker.swift in Sources */,
|
||||
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
|
||||
B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */,
|
||||
D7DB930B2D69486700DA1EE5 /* NIP65.swift in Sources */,
|
||||
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
|
||||
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
|
||||
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
||||
@@ -5311,6 +5325,7 @@
|
||||
82D6FBE02CD99F7900C925F4 /* ReactionsSettingsView.swift in Sources */,
|
||||
82D6FBE12CD99F7900C925F4 /* NotificationSettingsView.swift in Sources */,
|
||||
82D6FBE22CD99F7900C925F4 /* AppearanceSettingsView.swift in Sources */,
|
||||
D7DB930A2D69486700DA1EE5 /* NIP65.swift in Sources */,
|
||||
82D6FBE32CD99F7900C925F4 /* KeySettingsView.swift in Sources */,
|
||||
82D6FBE42CD99F7900C925F4 /* ZapSettingsView.swift in Sources */,
|
||||
82D6FBE52CD99F7900C925F4 /* TranslationSettingsView.swift in Sources */,
|
||||
@@ -5715,6 +5730,7 @@
|
||||
D73E5F012C6A97F4007EB227 /* ZapTypePicker.swift in Sources */,
|
||||
D73E5F022C6A97F4007EB227 /* ZapUserView.swift in Sources */,
|
||||
D73E5F032C6A97F4007EB227 /* ProfileZapLinkView.swift in Sources */,
|
||||
D7DB930C2D69486700DA1EE5 /* NIP65.swift in Sources */,
|
||||
D73E5F042C6A97F4007EB227 /* AboutView.swift in Sources */,
|
||||
D73E5F052C6A97F4007EB227 /* ProfileName.swift in Sources */,
|
||||
D73E5F062C6A97F4007EB227 /* ProfilePictureSelector.swift in Sources */,
|
||||
|
||||
@@ -225,6 +225,8 @@ class HomeModel: ContactsDelegate {
|
||||
// TODO: Implement draft syncing with relays. We intentionally do not support that as of writing. See `DraftsModel.swift` for other details
|
||||
// try? damus_state.drafts.load(wrapped_draft_note: ev, with: damus_state)
|
||||
break
|
||||
case .relay_list:
|
||||
break // This will be handled by `UserRelayListManager`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -336,6 +336,10 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "draft_event_ids", default_value: nil)
|
||||
var draft_event_ids: [String]?
|
||||
|
||||
// TODO: Get rid of this once we have NostrDB query capabilities integrated
|
||||
@Setting(key: "latest_relay_list_event_id", default_value: nil)
|
||||
var latestRelayListEventIdHex: String?
|
||||
|
||||
// MARK: Helper types
|
||||
|
||||
enum NotificationsMode: String, CaseIterable, Identifiable, StringCodable, Equatable {
|
||||
|
||||
167
damus/NIP65/NIP65.swift
Normal file
167
damus/NIP65/NIP65.swift
Normal file
@@ -0,0 +1,167 @@
|
||||
//
|
||||
// NIP65.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-21.
|
||||
//
|
||||
// Some text excerpts taken from the Nostr Protocol itself (which are public domain)
|
||||
|
||||
import OrderedCollections
|
||||
import Foundation
|
||||
|
||||
/// Includes models and functions for working with NIP-65
|
||||
struct NIP65: Sendable {}
|
||||
|
||||
extension NIP65 {
|
||||
/// Models a NIP-65 relay list
|
||||
struct RelayList: NostrEventConvertible, Sendable {
|
||||
let relays: OrderedDictionary<RelayURL, RelayItem>
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(event: NdbNote) throws(NIP65DecodingError) {
|
||||
guard event.known_kind == .relay_list else { throw .notRelayList }
|
||||
var relays: [RelayItem] = []
|
||||
for tag in event.tags {
|
||||
guard let relay = try RelayItem.fromTag(tag: tag) else { continue }
|
||||
relays.append(relay)
|
||||
}
|
||||
self.relays = Self.relayOrderedDictionary(from: relays)
|
||||
}
|
||||
|
||||
init?(event: NdbNote?) throws(NIP65DecodingError) {
|
||||
guard let event else { return nil }
|
||||
try self.init(event: event)
|
||||
}
|
||||
|
||||
init(relays: [RelayItem]) {
|
||||
self.relays = Self.relayOrderedDictionary(from: relays)
|
||||
}
|
||||
|
||||
init(relays: [RelayURL]) {
|
||||
let relayItemList = relays.map({ RelayItem(url: $0, rwConfiguration: .readWrite) })
|
||||
self.relays = Self.relayOrderedDictionary(from: relayItemList)
|
||||
}
|
||||
|
||||
private static func relayOrderedDictionary(from relayList: [RelayItem]) -> OrderedDictionary<RelayURL, RelayItem> {
|
||||
var seenUrls: Set<RelayURL> = []
|
||||
return OrderedDictionary(uniqueKeysWithValues: relayList.compactMap({
|
||||
// We need to ensure the keys are unique to avoid assertion errors from OrderedDictionary
|
||||
guard !seenUrls.contains($0.url) else { return nil }
|
||||
seenUrls.insert($0.url)
|
||||
return ($0.url, $0)
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Conversion to a Nostr Event
|
||||
|
||||
func toNostrEvent(keypair: FullKeypair, timestamp: UInt32? = nil) -> NostrEvent? {
|
||||
return NdbNote(
|
||||
content: "",
|
||||
keypair: keypair.to_keypair(),
|
||||
kind: NostrKind.relay_list.rawValue,
|
||||
tags: self.relays.values.map({ $0.tag }),
|
||||
createdAt: timestamp ?? UInt32(Date.now.timeIntervalSince1970)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NIP65 {
|
||||
/// An error thrown when decoding an item into a NIP-65 relay list
|
||||
enum NIP65DecodingError: Error {
|
||||
/// The Nostr event being converted is not a NIP-65 relay list
|
||||
case notRelayList
|
||||
/// The relay URL is invalid
|
||||
case invalidRelayURL
|
||||
///The relay RW marker is invalid
|
||||
case invalidRelayMarker
|
||||
}
|
||||
}
|
||||
|
||||
extension NIP65.RelayList {
|
||||
/// An item referencing a relay and its configuration inside a relay list
|
||||
struct RelayItem: ThrowingTagConvertible, Sendable {
|
||||
typealias E = NIP65.NIP65DecodingError
|
||||
|
||||
let url: RelayURL
|
||||
let rwConfiguration: RWConfiguration
|
||||
|
||||
/// The raw tag sequence in a Nostr event
|
||||
var tag: [String] {
|
||||
var tag = ["r", url.absoluteString]
|
||||
if let rwMarker = rwConfiguration.tagItem { tag.append(rwMarker) }
|
||||
return tag
|
||||
}
|
||||
|
||||
/// Initialize a new relay item from a Nostr event's tag sequence
|
||||
static func fromTag(tag: TagSequence) throws(E) -> NIP65.RelayList.RelayItem? {
|
||||
var i = tag.makeIterator()
|
||||
|
||||
guard tag.count >= 2,
|
||||
let t0 = i.next(),
|
||||
let key = t0.single_char,
|
||||
let rkey = RefId.RefKey(rawValue: key),
|
||||
let t1 = i.next()
|
||||
else { return nil }
|
||||
|
||||
let t2 = i.next()
|
||||
|
||||
switch rkey {
|
||||
case .r: return try self.fromRawInfo(urlString: t1.string(), rwMarker: t2?.string())
|
||||
// Keep options explicit to make compiler prompt developer on whether to ignore or handle new future options
|
||||
case .e, .p, .q, .t, .d, .a: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes a Relay Item based on raw information
|
||||
static func fromRawInfo(urlString: String, rwMarker: String?) throws(NIP65.NIP65DecodingError) -> NIP65.RelayList.RelayItem? {
|
||||
guard let relayUrl = RelayURL(urlString) else { throw .invalidRelayURL }
|
||||
guard let rwConfiguration = RWConfiguration.fromTagItem(rwMarker) else { throw .invalidRelayMarker }
|
||||
return NIP65.RelayList.RelayItem(url: relayUrl, rwConfiguration: rwConfiguration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NIP65.RelayList.RelayItem {
|
||||
/// The read/write configuration for a relay item
|
||||
enum RWConfiguration: TagItemConvertible {
|
||||
case read
|
||||
case write
|
||||
case readWrite
|
||||
|
||||
static let READ_MARKER: String = "read"
|
||||
static let WRITE_MARKER: String = "write"
|
||||
|
||||
var canRead: Bool {
|
||||
switch self {
|
||||
case .read, .readWrite: return true
|
||||
case .write: return false
|
||||
}
|
||||
}
|
||||
|
||||
var canWrite: Bool {
|
||||
switch self {
|
||||
case .write, .readWrite: return true
|
||||
case .read: return false
|
||||
}
|
||||
}
|
||||
|
||||
/// A raw Nostr Event tag item
|
||||
var tagItem: String? {
|
||||
switch self {
|
||||
case .read: Self.READ_MARKER
|
||||
case .write: Self.WRITE_MARKER
|
||||
case .readWrite: nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize this from a raw Nostr Event tag item
|
||||
static func fromTagItem(_ item: String?) -> Self? {
|
||||
if item == READ_MARKER { return .read }
|
||||
if item == WRITE_MARKER { return .write }
|
||||
return .readWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,19 @@ protocol TagConvertible {
|
||||
static func from_tag(tag: TagSequence) -> Self?
|
||||
}
|
||||
|
||||
/// Protocol for types that can be converted from/to a tag sequence with the possibilty of an error
|
||||
protocol ThrowingTagConvertible {
|
||||
associatedtype E: Error
|
||||
var tag: [String] { get }
|
||||
static func fromTag(tag: TagSequence) throws(E) -> Self?
|
||||
}
|
||||
|
||||
/// Protocol for types that can be converted from/to a tag item
|
||||
protocol TagItemConvertible {
|
||||
var tagItem: String? { get }
|
||||
static func fromTagItem(_ item: String?) -> Self?
|
||||
}
|
||||
|
||||
struct QuoteId: IdType, TagKey, TagConvertible {
|
||||
let id: Data
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ enum NostrKind: UInt32, Codable {
|
||||
case like = 7
|
||||
case chat = 42
|
||||
case mute_list = 10000
|
||||
case relay_list = 10002
|
||||
case list_deprecated = 30000
|
||||
case draft = 31234
|
||||
case longform = 30023
|
||||
|
||||
@@ -17,6 +17,15 @@ public struct LegacyKind3RelayRWConfiguration: Codable, Sendable {
|
||||
}
|
||||
|
||||
static let rw = LegacyKind3RelayRWConfiguration(read: true, write: true)
|
||||
|
||||
func toNIP65RWConfiguration() -> NIP65.RelayList.RelayItem.RWConfiguration? {
|
||||
switch (self.read, self.write) {
|
||||
case (false, true): return .write
|
||||
case (true, false): return .read
|
||||
case (true, true): return .readWrite
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RelayVariant {
|
||||
@@ -162,3 +171,26 @@ extension RelayPool {
|
||||
case RelayAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Extension to bridge NIP-65 relay list structs with app-native objects
|
||||
|
||||
extension NIP65.RelayList {
|
||||
static func fromLegacyContactList(_ contactList: NdbNote) throws(BridgeError) -> Self {
|
||||
guard let relayListInfo = decode_json_relays(contactList.content) else { throw .couldNotDecodeRelayListInfo }
|
||||
let relayItems = relayListInfo.map({ url, rwConfiguration in
|
||||
return RelayItem(url: url, rwConfiguration: rwConfiguration.toNIP65RWConfiguration() ?? .readWrite)
|
||||
})
|
||||
return NIP65.RelayList(relays: relayItems)
|
||||
}
|
||||
|
||||
static func fromLegacyContactList(_ contactList: NdbNote?) throws(BridgeError) -> Self? {
|
||||
guard let contactList = contactList else { return nil }
|
||||
return try fromLegacyContactList(contactList)
|
||||
}
|
||||
|
||||
enum BridgeError: Error {
|
||||
case couldNotDecodeRelayListInfo
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ class LoadableNostrEventViewModel: ObservableObject {
|
||||
case .zap, .zap_request:
|
||||
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
|
||||
return .loaded(route: Route.Zaps(target: zap.target))
|
||||
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status:
|
||||
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list:
|
||||
return .unknown_or_unsupported_kind
|
||||
}
|
||||
case .naddr(let naddr):
|
||||
|
||||
Reference in New Issue
Block a user