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