damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 8785f31834266e2d12dfc1a797f29f6dc805e268
parent 104205394fe5e441644cd64d934940712dd90b77
Author: William Casarin <jb55@jb55.com>
Date:   Wed,  4 Jan 2023 01:30:37 -0800

NIP05 Verification

Changelog-Added: Added NIP05 Verification

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/Models/HomeModel.swift | 16++++++++++++++++
Mdamus/Nostr/Profiles.swift | 5+++++
Adamus/Util/NIP05.swift | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/ChatView.swift | 2+-
Mdamus/Views/DMChatView.swift | 2+-
Mdamus/Views/EditMetadataView.swift | 13-------------
Mdamus/Views/EventView.swift | 4++--
Mdamus/Views/FollowingView.swift | 2+-
Mdamus/Views/ProfileName.swift | 80+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mdamus/Views/ProfileView.swift | 7++++---
11 files changed, 149 insertions(+), 51 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ 4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; }; 4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */; }; 4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF4295E679D007FD187 /* UserRelaysView.swift */; }; + 4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838529656C8B00DC99E7 /* NIP05.swift */; }; 4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; }; 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; }; 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; }; @@ -304,6 +305,7 @@ 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profiles.swift; sourceTree = "<group>"; }; 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedRelayView.swift; sourceTree = "<group>"; }; 4CB55EF4295E679D007FD187 /* UserRelaysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRelaysView.swift; sourceTree = "<group>"; }; + 4CB8838529656C8B00DC99E7 /* NIP05.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05.swift; sourceTree = "<group>"; }; 4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; }; 4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; }; 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; }; @@ -550,6 +552,7 @@ 3165648A295B70D500C64604 /* LinkView.swift */, 4C3A1D3629637E0500558C0F /* PreviewCache.swift */, 64FBD06E296255C400D9D3B2 /* Theme.swift */, + 4CB8838529656C8B00DC99E7 /* NIP05.swift */, ); path = Util; sourceTree = "<group>"; @@ -874,6 +877,7 @@ 4C3EA64F28FF59F200C48A62 /* tal.c in Sources */, 4C8682872814DE470026224F /* ProfileView.swift in Sources */, 4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */, + 4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */, 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */, 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */, 4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */, diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -544,7 +544,9 @@ func process_metadata_event(profiles: Profiles, ev: NostrEvent) { return } + var old_nip05: String? = nil if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) { + old_nip05 = mprof.profile.nip05 if mprof.timestamp > ev.created_at { // skip if we already have an newer profile return @@ -554,6 +556,20 @@ func process_metadata_event(profiles: Profiles, ev: NostrEvent) { let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at) profiles.add(id: ev.pubkey, profile: tprof) + if let nip05 = profile.nip05, old_nip05 != profile.nip05 { + Task.detached(priority: .background) { + let validated = await validate_nip05(pubkey: ev.pubkey, nip05_str: nip05) + if validated != nil { + print("validated nip05 for '\(nip05)'") + } + + DispatchQueue.main.async { + profiles.validated[ev.pubkey] = validated + notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) + } + } + } + // load pfps asap let picture = tprof.profile.picture ?? robohash(ev.pubkey) if let _ = URL(string: picture) { diff --git a/damus/Nostr/Profiles.swift b/damus/Nostr/Profiles.swift @@ -11,6 +11,11 @@ import UIKit class Profiles { var profiles: [String: TimestampedProfile] = [:] + var validated: [String: NIP05] = [:] + + func is_validated(_ pk: String) -> NIP05? { + return validated[pk] + } func add(id: String, profile: TimestampedProfile) { profiles[id] = profile diff --git a/damus/Util/NIP05.swift b/damus/Util/NIP05.swift @@ -0,0 +1,65 @@ +// +// NIP05.swift +// damus +// +// Created by William Casarin on 2023-01-04. +// + +import Foundation + +struct NIP05 { + let username: String + let host: String + + var url: URL? { + URL(string: "https://\(host)/.well-known/nostr.json?name=\(username)") + } + + static func parse(_ nip05: String) -> NIP05? { + let parts = nip05.split(separator: "@") + guard parts.count == 2 else { + return nil + } + return NIP05(username: String(parts[0]), host: String(parts[1])) + } +} + + +struct NIP05Response: Decodable { + let names: [String: String] + let relays: [String: [String]]? +} + +enum NIP05Validation { + case invalid + case valid +} + +func validate_nip05(pubkey: String, nip05_str: String) async -> NIP05? { + guard let nip05 = NIP05.parse(nip05_str) else { + return nil + } + + guard let url = nip05.url else { + return nil + } + + guard let ret = try? await URLSession.shared.data(from: url) else { + return nil + } + let dat = ret.0 + + guard let decoded = try? JSONDecoder().decode(NIP05Response.self, from: dat) else { + return nil + } + + guard let stored_pk = decoded.names[nip05.username] else { + return nil + } + + guard stored_pk == pubkey else { + return nil + } + + return nip05 +} diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift @@ -86,7 +86,7 @@ struct ChatView: View { VStack(alignment: .leading) { if just_started { HStack { - ProfileName(pubkey: event.pubkey, profile: damus_state.profiles.lookup(id: event.pubkey), contacts: damus_state.contacts, show_friend_confirmed: true) + ProfileName(pubkey: event.pubkey, profile: damus_state.profiles.lookup(id: event.pubkey), contacts: damus_state.contacts, show_friend_confirmed: true, profiles: damus_state.profiles) .foregroundColor(colorScheme == .dark ? id_to_color(event.pubkey) : Color.black) //.shadow(color: Color.black, radius: 2) Text("\(format_relative_time(event.created_at))") diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift @@ -40,7 +40,7 @@ struct DMChatView: View { HStack { ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles) - ProfileName(pubkey: pubkey, profile: profile, contacts: damus_state.contacts, show_friend_confirmed: true) + ProfileName(pubkey: pubkey, profile: profile, contacts: damus_state.contacts, show_friend_confirmed: true, profiles: damus_state.profiles) } } .buttonStyle(PlainButtonStyle()) diff --git a/damus/Views/EditMetadataView.swift b/damus/Views/EditMetadataView.swift @@ -15,19 +15,6 @@ func isHttpsUrl(_ string: String) -> Bool { return urlTest.evaluate(with: string) } -struct NIP05 { - let username: String - let host: String - - static func parse(_ nip05: String) -> NIP05? { - let parts = nip05.split(separator: "@") - guard parts.count == 2 else { - return nil - } - return NIP05(username: String(parts[0]), host: String(parts[1])) - } -} - func isImage(_ urlString: String) -> Bool { let imageTypes = ["image/jpg", "image/jpeg", "image/png", "image/gif", "image/tiff", "image/bmp", "image/webp"] diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -176,7 +176,7 @@ struct EventView: View { Image(systemName: "arrow.2.squarepath") .font(.footnote.weight(.bold)) .foregroundColor(Color.gray) - ProfileName(pubkey: event.pubkey, profile: prof, contacts: damus.contacts, show_friend_confirmed: true) + ProfileName(pubkey: event.pubkey, profile: prof, contacts: damus.contacts, show_friend_confirmed: true, profiles: damus.profiles) .font(.footnote.weight(.bold)) .foregroundColor(Color.gray) Text("Boosted") @@ -227,7 +227,7 @@ struct EventView: View { } } - EventProfileName(pubkey: pubkey, profile: profile, contacts: damus.contacts, show_friend_confirmed: show_friend_icon, size: size) + EventProfileName(pubkey: pubkey, profile: profile, contacts: damus.contacts, show_friend_confirmed: show_friend_icon, profiles: damus.profiles, size: size) if size != .selected { Text("\(format_relative_time(event.created_at))") .font(eventviewsize_to_font(size)) diff --git a/damus/Views/FollowingView.swift b/damus/Views/FollowingView.swift @@ -24,7 +24,7 @@ struct FollowUserView: View { VStack(alignment: .leading) { let profile = damus_state.profiles.lookup(id: target.pubkey) - ProfileName(pubkey: target.pubkey, profile: profile, contacts: damus_state.contacts, show_friend_confirmed: false) + ProfileName(pubkey: target.pubkey, profile: profile, contacts: damus_state.contacts, show_friend_confirmed: false, profiles: damus_state.profiles) if let about = profile?.about { Text(FollowUserView.markdown.process(about)) .lineLimit(3) diff --git a/damus/Views/ProfileName.swift b/damus/Views/ProfileName.swift @@ -7,28 +7,6 @@ import SwiftUI -struct ProfileFullName: View { - let pubkey: String - let profile: Profile? - let contacts: Contacts - - @State var display_name: String? - - var body: some View { - HStack { - if let real_name = profile?.display_name { - Text(real_name) - .bold() - ProfileName(pubkey: pubkey, profile: profile, prefix: "@", contacts: contacts, show_friend_confirmed: true) - .font(.footnote) - .foregroundColor(.gray) - } else { -// ProfileName(pubkey: pubkey, profile: profile, contacts: contacts, show_friend_confirmed: true) - } - } - } -} - struct ProfileName: View { let pubkey: String let profile: Profile? @@ -36,23 +14,27 @@ struct ProfileName: View { let prefix: String let show_friend_confirmed: Bool + let profiles: Profiles @State var display_name: String? + @State var nip05: NIP05? - init(pubkey: String, profile: Profile?, contacts: Contacts, show_friend_confirmed: Bool) { + init(pubkey: String, profile: Profile?, contacts: Contacts, show_friend_confirmed: Bool, profiles: Profiles) { self.pubkey = pubkey self.profile = profile self.prefix = "" self.contacts = contacts self.show_friend_confirmed = show_friend_confirmed + self.profiles = profiles } - init(pubkey: String, profile: Profile?, prefix: String, contacts: Contacts, show_friend_confirmed: Bool) { + init(pubkey: String, profile: Profile?, prefix: String, contacts: Contacts, show_friend_confirmed: Bool, profiles: Profiles) { self.pubkey = pubkey self.profile = profile self.prefix = prefix self.contacts = contacts self.show_friend_confirmed = show_friend_confirmed + self.profiles = profiles } var friend_icon: String? { @@ -71,12 +53,26 @@ struct ProfileName: View { return nil } + var nip05_color: Color { + contacts.is_friend(pubkey) ? .blue : .yellow + } + + var current_nip05: NIP05? { + nip05 ?? profiles.is_validated(pubkey) + } + var body: some View { - HStack { + HStack(spacing: 2) { Text(prefix + String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))) .font(.body) .fontWeight(prefix == "@" ? .none : .bold) - if let friend = friend_icon { + if let nip05 = current_nip05 { + Image(systemName: "checkmark.seal.fill") + .foregroundColor(nip05_color) + Text(nip05.host) + .foregroundColor(nip05_color) + } + if let friend = friend_icon, current_nip05 == nil { Image(systemName: friend) .foregroundColor(.gray) } @@ -87,6 +83,7 @@ struct ProfileName: View { return } display_name = Profile.displayName(profile: update.profile, pubkey: pubkey) + nip05 = profiles.is_validated(pubkey) } } } @@ -99,27 +96,31 @@ struct EventProfileName: View { let prefix: String let show_friend_confirmed: Bool + let profiles: Profiles @State var display_name: String? + @State var nip05: NIP05? let size: EventViewKind - init(pubkey: String, profile: Profile?, contacts: Contacts, show_friend_confirmed: Bool, size: EventViewKind = .normal) { + init(pubkey: String, profile: Profile?, contacts: Contacts, show_friend_confirmed: Bool, profiles: Profiles, size: EventViewKind = .normal) { self.pubkey = pubkey self.profile = profile self.prefix = "" self.contacts = contacts self.show_friend_confirmed = show_friend_confirmed self.size = size + self.profiles = profiles } - init(pubkey: String, profile: Profile?, prefix: String, contacts: Contacts, show_friend_confirmed: Bool, size: EventViewKind = .normal) { + init(pubkey: String, profile: Profile?, prefix: String, contacts: Contacts, show_friend_confirmed: Bool, profiles: Profiles, size: EventViewKind = .normal) { self.pubkey = pubkey self.profile = profile self.prefix = prefix self.contacts = contacts self.show_friend_confirmed = show_friend_confirmed self.size = size + self.profiles = profiles } var friend_icon: String? { @@ -137,12 +138,21 @@ struct EventProfileName: View { return nil } + + var nip05_color: Color { + contacts.is_friend(pubkey) ? .blue : .yellow + } + var current_nip05: NIP05? { + nip05 ?? profiles.is_validated(pubkey) + } + var body: some View { - HStack { + HStack(spacing: 2) { if let real_name = profile?.display_name { Text(real_name) .font(.body.weight(.bold)) + .padding([.trailing], 4) Text("@" + String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))) .foregroundColor(.gray) @@ -153,7 +163,16 @@ struct EventProfileName: View { .fontWeight(.bold) } - if let frend = friend_icon { + if let nip05 = current_nip05 { + Image(systemName: "checkmark.seal.fill") + .foregroundColor(nip05_color) + if !contacts.is_friend(pubkey) { + Text(nip05.host) + .foregroundColor(nip05_color) + } + } + + if let frend = friend_icon, current_nip05 == nil { Label("", systemImage: frend) .foregroundColor(.gray) .font(.footnote) @@ -165,6 +184,7 @@ struct EventProfileName: View { return } display_name = Profile.displayName(profile: update.profile, pubkey: pubkey) + nip05 = profiles.is_validated(pubkey) } } } diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift @@ -49,6 +49,7 @@ struct ProfileNameView: View { let pubkey: String let profile: Profile? let contacts: Contacts + let profiles: Profiles var body: some View { Group { @@ -56,7 +57,7 @@ struct ProfileNameView: View { VStack(alignment: .leading) { Text(real_name) .font(.title3.weight(.bold)) - ProfileName(pubkey: pubkey, profile: profile, prefix: "@", contacts: contacts, show_friend_confirmed: true) + ProfileName(pubkey: pubkey, profile: profile, prefix: "@", contacts: contacts, show_friend_confirmed: true, profiles: profiles) .font(.callout) .foregroundColor(.gray) KeyView(pubkey: pubkey) @@ -64,7 +65,7 @@ struct ProfileNameView: View { } } else { VStack(alignment: .leading) { - ProfileName(pubkey: pubkey, profile: profile, contacts: contacts, show_friend_confirmed: true) + ProfileName(pubkey: pubkey, profile: profile, contacts: contacts, show_friend_confirmed: true, profiles: profiles) .font(.title3.weight(.bold)) KeyView(pubkey: pubkey) .pubkey_context_menu(bech32_pubkey: pubkey) @@ -227,7 +228,7 @@ struct ProfileView: View { .offset(y: -15.0) // Increase if set a frame } - ProfileNameView(pubkey: profile.pubkey, profile: data, contacts: damus_state.contacts) + ProfileNameView(pubkey: profile.pubkey, profile: data, contacts: damus_state.contacts, profiles: damus_state.profiles) //.padding(.bottom) .padding(.top,-(pfp_size/2.0))