damus

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

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 }