ProfileModel.swift (8442B)
1 // 2 // ProfileModel.swift 3 // damus 4 // 5 // Created by William Casarin on 2022-04-27. 6 // 7 8 import Foundation 9 10 class ProfileModel: ObservableObject, Equatable { 11 @Published var contacts: NostrEvent? = nil 12 @Published var following: Int = 0 13 @Published var relay_list: NIP65.RelayList? = nil 14 @Published var legacy_relay_list: [RelayURL: LegacyKind3RelayRWConfiguration]? = nil 15 @Published var progress: Int = 0 16 var relay_urls: [RelayURL]? { 17 if let relay_list { 18 return relay_list.relays.values.map({ $0.url }) 19 } 20 if let legacy_relay_list { 21 return Array(legacy_relay_list.keys) 22 } 23 return nil 24 } 25 26 var events: EventHolder 27 let pubkey: Pubkey 28 let damus: DamusState 29 30 var seen_event: Set<NoteId> = Set() 31 var sub_id = UUID().description 32 var prof_subid = UUID().description 33 var conversations_subid = UUID().description 34 var findRelay_subid = UUID().description 35 var conversation_events: Set<NoteId> = Set() 36 37 init(pubkey: Pubkey, damus: DamusState) { 38 self.pubkey = pubkey 39 self.damus = damus 40 self.events = EventHolder(on_queue: { ev in 41 preload_events(state: damus, events: [ev]) 42 }) 43 } 44 45 func follows(pubkey: Pubkey) -> Bool { 46 guard let contacts = self.contacts else { 47 return false 48 } 49 50 return contacts.referenced_pubkeys.contains(pubkey) 51 } 52 53 func get_follow_target() -> FollowTarget { 54 if let contacts = contacts { 55 return .contact(contacts) 56 } 57 return .pubkey(pubkey) 58 } 59 60 static func == (lhs: ProfileModel, rhs: ProfileModel) -> Bool { 61 return lhs.pubkey == rhs.pubkey 62 } 63 64 func hash(into hasher: inout Hasher) { 65 hasher.combine(pubkey) 66 } 67 68 func unsubscribe() { 69 print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)") 70 damus.nostrNetwork.pool.unsubscribe(sub_id: sub_id) 71 damus.nostrNetwork.pool.unsubscribe(sub_id: prof_subid) 72 if pubkey != damus.pubkey { 73 damus.nostrNetwork.pool.unsubscribe(sub_id: conversations_subid) 74 } 75 } 76 77 func subscribe() { 78 var text_filter = NostrFilter(kinds: [.text, .longform, .highlight]) 79 var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost]) 80 var relay_list_filter = NostrFilter(kinds: [.relay_list], authors: [pubkey]) 81 82 profile_filter.authors = [pubkey] 83 84 text_filter.authors = [pubkey] 85 text_filter.limit = 500 86 87 print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)") 88 //print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]]) 89 damus.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event) 90 damus.nostrNetwork.pool.subscribe(sub_id: prof_subid, filters: [profile_filter, relay_list_filter], handler: handle_event) 91 92 subscribe_to_conversations() 93 } 94 95 private func subscribe_to_conversations() { 96 // Only subscribe to conversation events if the profile is not us. 97 guard pubkey != damus.pubkey else { 98 return 99 } 100 101 let conversation_kinds: [NostrKind] = [.text, .longform, .highlight] 102 let limit: UInt32 = 500 103 let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey]) 104 let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey]) 105 print("subscribing to conversation events from and to profile \(pubkey) with sub_id \(conversations_subid)") 106 damus.nostrNetwork.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event) 107 } 108 109 func handle_profile_contact_event(_ ev: NostrEvent) { 110 process_contact_event(state: damus, ev: ev) 111 112 // only use new stuff 113 if let current_ev = self.contacts { 114 guard ev.created_at > current_ev.created_at else { 115 return 116 } 117 } 118 119 self.contacts = ev 120 self.following = count_pubkeys(ev.tags) 121 self.legacy_relay_list = decode_json_relays(ev.content) 122 } 123 124 private func add_event(_ ev: NostrEvent) { 125 if ev.is_textlike || ev.known_kind == .boost { 126 if self.events.insert(ev) { 127 self.objectWillChange.send() 128 } 129 } else if ev.known_kind == .contacts { 130 handle_profile_contact_event(ev) 131 } 132 else if ev.known_kind == .relay_list { 133 self.relay_list = try? NIP65.RelayList(event: ev) // Whether another user's list is malformatted is something beyond our control. Probably best to suppress errors 134 } 135 seen_event.insert(ev.id) 136 } 137 138 // Ensure the event public key matches the public key(s) we are querying. 139 // This is done to protect against a relay not properly filtering events by the pubkey 140 // See https://github.com/damus-io/damus/issues/1846 for more information 141 private func relay_filtered_correctly(_ ev: NostrEvent, subid: String?) -> Bool { 142 if subid == self.conversations_subid { 143 switch ev.pubkey { 144 case self.pubkey: 145 return ev.referenced_pubkeys.contains(damus.pubkey) 146 case damus.pubkey: 147 return ev.referenced_pubkeys.contains(self.pubkey) 148 default: 149 return false 150 } 151 } 152 153 return self.pubkey == ev.pubkey 154 } 155 156 private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) { 157 switch ev { 158 case .ws_connection_event: 159 return 160 case .nostr_event(let resp): 161 guard resp.subid == self.sub_id || resp.subid == self.prof_subid || resp.subid == self.conversations_subid else { 162 return 163 } 164 switch resp { 165 case .ok: 166 break 167 case .event(_, let ev): 168 guard ev.should_show_event else { 169 break 170 } 171 172 if !seen_event.contains(ev.id) { 173 guard relay_filtered_correctly(ev, subid: resp.subid) else { 174 break 175 } 176 177 add_event(ev) 178 179 if resp.subid == self.conversations_subid { 180 conversation_events.insert(ev.id) 181 } 182 } else if resp.subid == self.conversations_subid && !conversation_events.contains(ev.id) { 183 guard relay_filtered_correctly(ev, subid: resp.subid) else { 184 break 185 } 186 187 conversation_events.insert(ev.id) 188 } 189 case .notice: 190 break 191 //notify(.notice, notice) 192 case .eose: 193 guard let txn = NdbTxn(ndb: damus.ndb) else { return } 194 if resp.subid == sub_id { 195 load_profiles(context: "profile", profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus, txn: txn) 196 } 197 progress += 1 198 break 199 case .auth: 200 break 201 } 202 } 203 } 204 205 private func findRelaysHandler(relay_id: RelayURL, ev: NostrConnectionEvent) { 206 if case .nostr_event(let resp) = ev, case .event(_, let event) = resp, case .contacts = event.known_kind { 207 self.legacy_relay_list = decode_json_relays(event.content) 208 } 209 } 210 211 func subscribeToFindRelays() { 212 var profile_filter = NostrFilter(kinds: [.contacts]) 213 profile_filter.authors = [pubkey] 214 215 damus.nostrNetwork.pool.subscribe(sub_id: findRelay_subid, filters: [profile_filter], handler: findRelaysHandler) 216 } 217 218 func unsubscribeFindRelays() { 219 damus.nostrNetwork.pool.unsubscribe(sub_id: findRelay_subid) 220 } 221 222 func getCappedRelays() -> [RelayURL] { 223 return relay_list?.relays.keys.prefix(Constants.MAX_SHARE_RELAYS).map { $0 } ?? [] 224 } 225 } 226 227 228 func count_pubkeys(_ tags: Tags) -> Int { 229 var c: Int = 0 230 for tag in tags { 231 if tag.count >= 2 && tag[0].matches_char("p") { 232 c += 1 233 } 234 } 235 236 return c 237 }