Skip to content

Commit

Permalink
Merge branch 'main' into three-dots-menu-change
Browse files Browse the repository at this point in the history
  • Loading branch information
dcadenas authored Apr 19, 2024
2 parents 76d432f + 47143b6 commit 1dc0490
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 36 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

- Changed "Report note" button to "Flag this content"
- We are now publishing the relay list when registering a new NIP-05 username so
that other users can find you more easily.

## [0.1.11] - 2024-04-18Z

Expand Down
130 changes: 95 additions & 35 deletions Nos/Service/NamesAPI.swift
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -23,35 +24,42 @@ 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
}

private let verificationURL: URL
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
self.registrationURL = registrationURL
}

/// 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,
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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
}

Expand All @@ -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)
Expand All @@ -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 }
]
]
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 1dc0490

Please sign in to comment.