damus

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

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 }