From 1c5bd1cc4ecbda073518a59477791cf00253c17f Mon Sep 17 00:00:00 2001 From: Martin Dutra Date: Tue, 16 Apr 2024 14:35:42 -0300 Subject: [PATCH 1/2] Publish relays when registering a NIP05 username --- CHANGELOG.md | 2 + Nos/Service/NamesAPI.swift | 130 +++++++++++++----- .../ExcellentChoiceSheet.swift | 9 +- 3 files changed, 105 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b85283508..573951025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- We are now publishing the relay list when registering a new NIP-05 username so +that other users can find you more easily. - Fixed an issue where reports for notes were treated as reports for profiles. - Updated the Discover tab navigation bar to match new design. diff --git a/Nos/Service/NamesAPI.swift b/Nos/Service/NamesAPI.swift index 6819a7844..2a9298e3e 100644 --- a/Nos/Service/NamesAPI.swift +++ b/Nos/Service/NamesAPI.swift @@ -1,7 +1,8 @@ import Foundation -/// The NamesAPI service is in charge of creating and deleting nos.social usernames and -/// verifying if a NIP-05 or nos.social username can be associated or not. +/// The NamesAPI service is in charge of creating and deleting nos.social +/// usernames and verifying if a NIP-05 or nos.social username can be associated +/// or not. class NamesAPI { private enum Error: LocalizedError { @@ -23,16 +24,18 @@ class NamesAPI { case post = "POST" } - /// Structure that encapsulates the result of requesting the `/.well-known/nostr.json` - /// path to a given server. + /// Structure that encapsulates the result of requesting the + /// `/.well-known/nostr.json` path to a given server. private enum PingResult { - /// The npub registered in the server matches with the provided public key + /// The npub registered in the server matches with the provided public + /// key. case match - /// The npub registered in the server doesn't match with the provided public key + /// The npub registered in the server doesn't match with the provided + /// public key. case mismatch - /// The server returned a 404 Not Found response + /// The server returned a 404 Not Found response. case notFound - /// The server returned an unexpected response + /// The server returned an unexpected response. case unableToPing } @@ -40,10 +43,12 @@ class NamesAPI { private let registrationURL: URL init?(host: String = "nos.social") { - guard let verificationURL = URL(string: "https://\(host)/.well-known/nostr.json") else { + let verificationURLString = "https://\(host)/.well-known/nostr.json" + guard let verificationURL = URL(string: verificationURLString) else { return nil } - guard let registrationURL = URL(string: "https://\(host)/api/names") else { + let registrationURLString = "https://\(host)/api/names" + guard let registrationURL = URL(string: registrationURLString) else { return nil } self.verificationURL = verificationURL @@ -51,7 +56,10 @@ class NamesAPI { } /// Deletes a given username from `nos.social` - func delete(username: String, keyPair: KeyPair) async throws { + func delete( + username: String, + keyPair: KeyPair + ) async throws { let request = try buildURLRequest( url: registrationURL.appending(path: username), method: .delete, @@ -69,7 +77,10 @@ class NamesAPI { /// Verifies that a given username is free to claim in nos.social, or has /// already been claimed by the given `publicKey`. - func checkAvailability(username: String, publicKey: PublicKey) async throws -> Bool { + func checkAvailability( + username: String, + publicKey: PublicKey + ) async throws -> Bool { let result = try await ping( username: username, host: verificationURL, @@ -78,8 +89,12 @@ class NamesAPI { return result == .match || result == .notFound } - /// Verifies that a given NIP-05 username is properly connected to the public key - func verify(username: String, publicKey: PublicKey) async throws -> Bool { + /// Verifies that a given NIP-05 username is properly connected to the + /// public key. + func verify( + username: String, + publicKey: PublicKey + ) async throws -> Bool { let components = username.components(separatedBy: "@") guard components.count == 2 else { @@ -89,7 +104,8 @@ class NamesAPI { let localPart = components[0] let domain = components[1] - guard let host = URL(string: "https://\(domain)/.well-known/nostr.json") else { + let hostString = "https://\(domain)/.well-known/nostr.json" + guard let host = URL(string: hostString) else { return false } @@ -102,11 +118,19 @@ class NamesAPI { } /// Registers a given username at `nos.social` - func register(username: String, keyPair: KeyPair) async throws { + func register( + username: String, + keyPair: KeyPair, + relays: [URL] + ) async throws { let request = try buildURLRequest( url: registrationURL, method: .post, - json: try buildJSON(username: username, keyPair: keyPair), + json: try buildJSON( + username: username, + keyPair: keyPair, + relays: relays + ), keyPair: keyPair ) let (_, response) = try await URLSession.shared.data(for: request) @@ -120,53 +144,89 @@ class NamesAPI { throw Error.unexpected } - /// Makes a request to `/.well-known/nostr.json` at the host with the provided - /// username and matches the server result with the provided public key. + /// Makes a request to `/.well-known/nostr.json` at the host with the + /// provided username and matches the server result with the provided public + /// key. private func ping( username: String, host: URL, publicKey: PublicKey ) async throws -> PingResult { let request = URLRequest( - url: host.appending(queryItems: [URLQueryItem(name: "name", value: username)]) + url: host.appending( + queryItems: [URLQueryItem(name: "name", value: username)] + ) ) let (data, response) = try await URLSession.shared.data(for: request) if let response = response as? HTTPURLResponse { let statusCode = response.statusCode if statusCode == 404 { return .notFound - } else if statusCode == 200, let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { - let names = json["names"] as? [String: String] - let npub = names?[username] - if npub == publicKey.hex { - return .match - } else { - return .mismatch + } else if statusCode == 200 { + let jsonObject = try JSONSerialization.jsonObject(with: data) + if let json = jsonObject as? [String: Any] { + let names = json["names"] as? [String: String] + let npub = names?[username] + if npub == publicKey.hex { + return .match + } else { + return .mismatch + } } } } return .unableToPing } - private func buildURLRequest(url: URL, method: HTTPMethod, json: Data?, keyPair: KeyPair) throws -> URLRequest { + private func buildURLRequest( + url: URL, + method: HTTPMethod, + json: Data?, + keyPair: KeyPair + ) throws -> URLRequest { let content = "" - let tags = [["u", url.absoluteString], ["method", method.rawValue]] - var jsonEvent = JSONEvent(pubKey: keyPair.publicKeyHex, kind: .auth, tags: tags, content: content) + let tags = [ + ["u", url.absoluteString], + ["method", method.rawValue] + ] + var jsonEvent = JSONEvent( + pubKey: keyPair.publicKeyHex, + kind: .auth, + tags: tags, + content: content + ) try jsonEvent.sign(withKey: keyPair) - let requestData = try JSONSerialization.data(withJSONObject: jsonEvent.dictionary) + let jsonObject = jsonEvent.dictionary + let requestData = try JSONSerialization.data(withJSONObject: jsonObject) var request = URLRequest(url: url) request.httpMethod = method.rawValue - request.setValue("Nostr \(requestData.base64EncodedString())", forHTTPHeaderField: "Authorization") + request.setValue( + "Nostr \(requestData.base64EncodedString())", + forHTTPHeaderField: "Authorization" + ) if let json { - request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue( + "application/json", + forHTTPHeaderField: "Content-Type" + ) request.httpBody = json } return request } - private func buildJSON(username: String, keyPair: KeyPair) throws -> Data { + private func buildJSON( + username: String, + keyPair: KeyPair, + relays: [URL] = [] + ) throws -> Data { try JSONSerialization.data( - withJSONObject: ["name": username, "data": ["pubkey": keyPair.publicKeyHex]] + withJSONObject: [ + "name": username, + "data": [ + "pubkey": keyPair.publicKeyHex, + "relays": relays.map { $0.absoluteString } + ] + ] ) } } diff --git a/Nos/Views/ProfileEdit/CreateUsernameWizard/ExcellentChoiceSheet.swift b/Nos/Views/ProfileEdit/CreateUsernameWizard/ExcellentChoiceSheet.swift index ced88213e..2ebbebc1b 100644 --- a/Nos/Views/ProfileEdit/CreateUsernameWizard/ExcellentChoiceSheet.swift +++ b/Nos/Views/ProfileEdit/CreateUsernameWizard/ExcellentChoiceSheet.swift @@ -103,7 +103,14 @@ struct ExcellentChoiceSheet: View { claimState = .claiming do { - try await namesAPI.register(username: username, keyPair: keyPair) + let relays = currentUser.author?.relays.compactMap { + $0.addressURL + } + try await namesAPI.register( + username: username, + keyPair: keyPair, + relays: relays ?? [] + ) currentUser.author?.nip05 = "\(username)@nos.social" try currentUser.viewContext.saveIfNeeded() await currentUser.publishMetaData() From 9c9dcfd33b4e89fceb1589489db7a8382e116db6 Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Thu, 18 Apr 2024 16:40:43 -0400 Subject: [PATCH 2/2] Ignore the .git folder for SwiftLint --- .swiftlint.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.swiftlint.yml b/.swiftlint.yml index dad7678b7..1b8423472 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -81,6 +81,7 @@ excluded: # paths to ignore during linting. Takes precedence over `included`. - Architecture/ - StarscreamOld/ - "**/.build" + - .git analyzer_rules: # Rules run by `swiftlint analyze` (experimental) - explicit_self - unused_import