Skip to content

Commit

Permalink
Merge pull request #974 from planetary-social/connect_nip05
Browse files Browse the repository at this point in the history
External NIP-05 connection
  • Loading branch information
martindsq authored Apr 10, 2024
2 parents 88eb86b + a82903d commit e17608b
Show file tree
Hide file tree
Showing 13 changed files with 543 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Added option to connect your existing NIP-05 username.
- Fixed a crash that often occurred after opening the app.

## [0.1.8] - 2024-04-03Z
Expand Down
17 changes: 17 additions & 0 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@
5B8C96AC29D52AD200B73AEC /* AuthorListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8C96AB29D52AD200B73AEC /* AuthorListView.swift */; };
5B8C96B029DB2E1100B73AEC /* SearchTextFieldObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8C96AF29DB2E1100B73AEC /* SearchTextFieldObserver.swift */; };
5B8C96B229DB313300B73AEC /* AuthorCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8C96B129DB313300B73AEC /* AuthorCard.swift */; };
5B8C96B629DDD3B200B73AEC /* EditableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8C96B529DDD3B200B73AEC /* EditableText.swift */; };
5BBA5E912BADF98E00D57D76 /* AlreadyHaveANIP05View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBA5E902BADF98E00D57D76 /* AlreadyHaveANIP05View.swift */; };
5BBA5E9C2BAE052F00D57D76 /* NiceWorkSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBA5E9B2BAE052F00D57D76 /* NiceWorkSheet.swift */; };
5B8C96B629DDD3B200B73AEC /* NoteTextViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8C96B529DDD3B200B73AEC /* NoteTextViewRepresentable.swift */; };
5BC0D9CC2B867B9D005D6980 /* NamesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC0D9CB2B867B9D005D6980 /* NamesAPI.swift */; };
5BD08BB22A38E96F00BB926C /* JSONRelayMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B503F612A291A1A0098805A /* JSONRelayMetadata.swift */; };
Expand Down Expand Up @@ -495,6 +498,8 @@
5B8C96B129DB313300B73AEC /* AuthorCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorCard.swift; sourceTree = "<group>"; };
5B8C96B529DDD3B200B73AEC /* NoteTextViewRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteTextViewRepresentable.swift; sourceTree = "<group>"; };
5B960D2C2B34B1B900C52C45 /* Nos 14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Nos 14.xcdatamodel"; sourceTree = "<group>"; };
5BBA5E902BADF98E00D57D76 /* AlreadyHaveANIP05View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlreadyHaveANIP05View.swift; sourceTree = "<group>"; };
5BBA5E9B2BAE052F00D57D76 /* NiceWorkSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NiceWorkSheet.swift; sourceTree = "<group>"; };
5BC0D9CB2B867B9D005D6980 /* NamesAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamesAPI.swift; sourceTree = "<group>"; };
5BE281BD2AE2CCAE00880466 /* StoriesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoriesView.swift; sourceTree = "<group>"; };
5BE281C02AE2CCB400880466 /* StoryNoteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryNoteView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -867,6 +872,7 @@
5B79F61A2B98B774002DA9BE /* CreateUsernameWizard */ = {
isa = PBXGroup;
children = (
5BBA5E962BAE04EB00D57D76 /* ExternalNIP05 */,
5B7C93AF2B6AD52400410ABE /* CreateUsernameWizard.swift */,
5B79F6082B98AC33002DA9BE /* ClaimYourUniqueIdentitySheet.swift */,
5B79F60A2B98ACA0002DA9BE /* PickYourUsernameSheet.swift */,
Expand Down Expand Up @@ -896,6 +902,15 @@
path = Components;
sourceTree = "<group>";
};
5BBA5E962BAE04EB00D57D76 /* ExternalNIP05 */ = {
isa = PBXGroup;
children = (
5BBA5E902BADF98E00D57D76 /* AlreadyHaveANIP05View.swift */,
5BBA5E9B2BAE052F00D57D76 /* NiceWorkSheet.swift */,
);
path = ExternalNIP05;
sourceTree = "<group>";
};
C9032C2C2BAE31DA001F4EC6 /* Profile */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1680,6 +1695,7 @@
C9C2B78529E073E300548B4A /* RelaySubscription.swift in Sources */,
3FFB1D9C29A7DF9D002A755D /* StackedAvatarsView.swift in Sources */,
C97A1C8E29E58EC7009D9E8D /* NSManagedObjectContext+Nos.swift in Sources */,
5BBA5E9C2BAE052F00D57D76 /* NiceWorkSheet.swift in Sources */,
C9B678DE29EEC35B00303F33 /* Foundation+Sendable.swift in Sources */,
5B88051A2A21027C00E21F06 /* SHA256Key.swift in Sources */,
C9CDBBA129A8F14C00C555C7 /* DiscoverView.swift in Sources */,
Expand Down Expand Up @@ -1770,6 +1786,7 @@
C9F84C27298DC98800C6714D /* KeyPair.swift in Sources */,
5B8C96B629DDD3B200B73AEC /* NoteTextViewRepresentable.swift in Sources */,
C93EC2F129C337EB0012EE2A /* RelayPicker.swift in Sources */,
5BBA5E912BADF98E00D57D76 /* AlreadyHaveANIP05View.swift in Sources */,
C9F0BB6F29A50437000547FC /* NostrConstants.swift in Sources */,
C96D39272B61B6D200D3D0A1 /* RawNostrID.swift in Sources */,
5BFF66B12A573F6400AA79DD /* RelayDetailView.swift in Sources */,
Expand Down
77 changes: 77 additions & 0 deletions Nos/Assets/Localization/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,17 @@
}
}
},
"alreadyHaveANIP05" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "No, thanks. I already have a NIP-05"
}
}
}
},
"amount" : {
"extractionState" : "manual",
"localizations" : {
Expand Down Expand Up @@ -5365,6 +5376,28 @@
}
}
},
"linkYourNIP05Description" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Enter your existing NIP-05 here:"
}
}
}
},
"linkYourNIP05Title" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Already have a NIP-05?"
}
}
}
},
"loading" : {
"extractionState" : "manual",
"localizations" : {
Expand Down Expand Up @@ -6711,6 +6744,17 @@
}
}
},
"niceWork" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nice work!"
}
}
}
},
"nip05" : {
"extractionState" : "manual",
"localizations" : {
Expand Down Expand Up @@ -6782,6 +6826,28 @@
}
}
},
"nip05Connected" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Your NIP-05 is now connected to your npub in the Nostr network."
}
}
}
},
"nip05Example" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "[email protected]"
}
}
}
},
"nip05LearnMore" : {
"extractionState" : "manual",
"localizations" : {
Expand Down Expand Up @@ -6823,6 +6889,17 @@
}
}
},
"nip05LinkFailed" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "**The NIP05 entered does not match the records of the NIP-05 provider.**\n\nPlease verify the entry of the NIP-05 and try again. Or reach out to the NIP05 provider directly to verify your NIP05 is live."
}
}
}
},
"no" : {
"extractionState" : "manual",
"localizations" : {
Expand Down
4 changes: 4 additions & 0 deletions Nos/Service/Analytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ class Analytics {
track("Registered NIP-05 Username")
}

func linkedNIP05Username() {
track("Linked NIP-05 Username")
}

func deletedNIP05Username() {
track("Deleted NIP-05 Username")
}
Expand Down
89 changes: 75 additions & 14 deletions Nos/Service/NamesAPI.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
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.
class NamesAPI {

private enum Error: LocalizedError {
Expand All @@ -21,6 +23,19 @@ class NamesAPI {
case post = "POST"
}

/// 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
case match
/// The npub registered in the server doesn't match with the provided public key
case mismatch
/// The server returned a 404 Not Found response
case notFound
/// The server returned an unexpected response
case unableToPing
}

private let verificationURL: URL
private let registrationURL: URL

Expand All @@ -35,6 +50,7 @@ class NamesAPI {
self.registrationURL = registrationURL
}

/// Deletes a given username from `nos.social`
func delete(username: String, keyPair: KeyPair) async throws {
let request = try buildURLRequest(
url: registrationURL.appending(path: username),
Expand All @@ -51,24 +67,41 @@ class NamesAPI {
throw Error.unexpected
}

func verify(username: String, keyPair: KeyPair) async throws -> Bool {
let request = URLRequest(
url: verificationURL.appending(queryItems: [URLQueryItem(name: "name", value: username)])
/// 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 {
let result = try await ping(
username: username,
host: verificationURL,
publicKey: publicKey
)
let (data, response) = try await URLSession.shared.data(for: request)
if let response = response as? HTTPURLResponse {
let statusCode = response.statusCode
if statusCode == 404 {
return true
} 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]
return npub == keyPair.publicKeyHex
}
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 {
let components = username.components(separatedBy: "@")

guard components.count == 2 else {
return false
}

let localPart = components[0]
let domain = components[1]

guard let host = URL(string: "https://\(domain)/.well-known/nostr.json") else {
return false
}
return false

let result = try await ping(
username: localPart,
host: host,
publicKey: publicKey
)
return result == .match
}

/// Registers a given username at `nos.social`
func register(username: String, keyPair: KeyPair) async throws {
let request = try buildURLRequest(
url: registrationURL,
Expand All @@ -87,6 +120,34 @@ 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.
private func ping(
username: String,
host: URL,
publicKey: PublicKey
) async throws -> PingResult {
let request = URLRequest(
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
}
}
}
return .unableToPing
}

private func buildURLRequest(url: URL, method: HTTPMethod, json: Data?, keyPair: KeyPair) throws -> URLRequest {
let content = ""
let tags = [["u", url.absoluteString], ["method", method.rawValue]]
Expand Down
5 changes: 0 additions & 5 deletions Nos/Service/Relay/RelayService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -714,11 +714,6 @@ extension RelayService: WebSocketDelegate {

// MARK: NIP-05 and UNS Support
extension RelayService {

func verifyNIP05(identifier: String, userPublicKey: RawAuthorID) async -> Bool {
let internetIdentifierPublicKey = await retrievePublicKeyFromUsername(identifier)
return internetIdentifierPublicKey == userPublicKey
}

/// Takes a NIP-05 or Mastodon username and tries to fetch the associated Nostr public key.
func retrievePublicKeyFromUsername(_ userName: String) async -> RawAuthorID? {
Expand Down
14 changes: 10 additions & 4 deletions Nos/Views/Components/WizardSheetDescriptionText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,26 @@ struct WizardSheetDescriptionText: View {
self.description = .plainText(localizedStringResource)
}

init(_ attributedString: AttributedString) {
init(_ attributedString: AttributedString, tint: Color) {
self.description = .markdown(attributedString
.replacingAttributes(
AttributeContainer(
[.inlinePresentationIntent: InlinePresentationIntent.stronglyEmphasized.rawValue]
),
with: AttributeContainer(
[.foregroundColor: UIColor.primaryTxt]
[.foregroundColor: UIColor(tint)]
)
))
}

init(markdown localizedStringResource: LocalizedStringResource) {
self.init(AttributedString(localized: localizedStringResource))
init(
markdown localizedStringResource: LocalizedStringResource,
tint: Color = .primaryTxt
) {
self.init(
AttributedString(localized: localizedStringResource),
tint: tint
)
}

var body: some View {
Expand Down
Loading

0 comments on commit e17608b

Please sign in to comment.