HomeModel.swift (39118B)
1 // 2 // HomeModel.swift 3 // damus 4 // 5 // Created by William Casarin on 2022-05-24. 6 // 7 8 import Foundation 9 import UIKit 10 11 enum Resubscribe { 12 case following 13 case unfollowing(FollowRef) 14 } 15 16 enum HomeResubFilter { 17 case pubkey(Pubkey) 18 case hashtag(String) 19 20 init?(from: FollowRef) { 21 switch from { 22 case .hashtag(let ht): self = .hashtag(ht.string()) 23 case .pubkey(let pk): self = .pubkey(pk) 24 } 25 26 return nil 27 } 28 29 func filter(contacts: Contacts, ev: NostrEvent) -> Bool { 30 switch self { 31 case .pubkey(let pk): 32 return ev.pubkey == pk 33 case .hashtag(let ht): 34 if contacts.is_friend(ev.pubkey) { 35 return false 36 } 37 return ev.referenced_hashtags.contains(where: { ref_ht in 38 ht == ref_ht.hashtag 39 }) 40 } 41 } 42 } 43 44 class HomeModel { 45 // Don't trigger a user notification for events older than a certain age 46 static let event_max_age_for_notification: TimeInterval = EVENT_MAX_AGE_FOR_NOTIFICATION 47 48 var damus_state: DamusState 49 50 // NDBTODO: let's get rid of this entirely, let nostrdb handle it 51 var has_event: [String: Set<NoteId>] = [:] 52 var deleted_events: Set<NoteId> = Set() 53 var last_event_of_kind: [RelayURL: [UInt32: NostrEvent]] = [:] 54 var done_init: Bool = false 55 var incoming_dms: [NostrEvent] = [] 56 let dm_debouncer = Debouncer(interval: 0.5) 57 let resub_debouncer = Debouncer(interval: 3.0) 58 var should_debounce_dms = true 59 60 let home_subid = UUID().description 61 let contacts_subid = UUID().description 62 let notifications_subid = UUID().description 63 let dms_subid = UUID().description 64 let init_subid = UUID().description 65 let profiles_subid = UUID().description 66 67 var loading: Bool = false 68 69 var signal = SignalModel() 70 71 var notifications = NotificationsModel() 72 var notification_status = NotificationStatusModel() 73 var events: EventHolder = EventHolder() 74 var zap_button: ZapButtonModel = ZapButtonModel() 75 76 init() { 77 self.damus_state = DamusState.empty 78 self.setup_debouncer() 79 filter_events() 80 events.on_queue = preloader 81 //self.events = EventHolder(on_queue: preloader) 82 } 83 84 func preloader(ev: NostrEvent) { 85 preload_events(state: self.damus_state, events: [ev]) 86 } 87 88 var pool: RelayPool { 89 return damus_state.pool 90 } 91 92 var dms: DirectMessagesModel { 93 return damus_state.dms 94 } 95 96 func has_sub_id_event(sub_id: String, ev_id: NoteId) -> Bool { 97 if !has_event.keys.contains(sub_id) { 98 has_event[sub_id] = Set() 99 return false 100 } 101 102 return has_event[sub_id]!.contains(ev_id) 103 } 104 105 func setup_debouncer() { 106 // turn off debouncer after initial load 107 DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { 108 self.should_debounce_dms = false 109 } 110 } 111 112 func resubscribe(_ resubbing: Resubscribe) { 113 if self.should_debounce_dms { 114 // don't resub on initial load 115 return 116 } 117 118 print("hit resub debouncer") 119 120 resub_debouncer.debounce { 121 print("resub") 122 self.unsubscribe_to_home_filters() 123 124 switch resubbing { 125 case .following: 126 break 127 case .unfollowing(let r): 128 if let filter = HomeResubFilter(from: r) { 129 self.events.filter { ev in !filter.filter(contacts: self.damus_state.contacts, ev: ev) } 130 } 131 } 132 133 self.subscribe_to_home_filters() 134 } 135 } 136 137 @MainActor 138 func process_event(sub_id: String, relay_id: RelayURL, ev: NostrEvent) { 139 if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) { 140 return 141 } 142 143 let last_k = get_last_event_of_kind(relay_id: relay_id, kind: ev.kind) 144 if last_k == nil || ev.created_at > last_k!.created_at { 145 last_event_of_kind[relay_id]?[ev.kind] = ev 146 } 147 148 guard let kind = ev.known_kind else { 149 return 150 } 151 152 switch kind { 153 case .chat, .longform, .text: 154 handle_text_event(sub_id: sub_id, ev) 155 case .contacts: 156 handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev) 157 case .metadata: 158 // profile metadata processing is handled by nostrdb 159 break 160 case .list_deprecated: 161 handle_old_list_event(ev) 162 case .mute_list: 163 handle_mute_list_event(ev) 164 case .boost: 165 handle_boost_event(sub_id: sub_id, ev) 166 case .like: 167 handle_like_event(ev) 168 case .dm: 169 handle_dm(ev) 170 case .delete: 171 handle_delete_event(ev) 172 case .zap: 173 handle_zap_event(ev) 174 case .zap_request: 175 break 176 case .nwc_request: 177 break 178 case .nwc_response: 179 handle_nwc_response(ev, relay: relay_id) 180 case .http_auth: 181 break 182 case .status: 183 handle_status_event(ev) 184 } 185 } 186 187 @MainActor 188 func handle_status_event(_ ev: NostrEvent) { 189 guard let st = UserStatus(ev: ev) else { 190 return 191 } 192 193 // don't process expired events 194 if let expires = st.expires_at, Date.now >= expires { 195 return 196 } 197 198 let pdata = damus_state.profiles.profile_data(ev.pubkey) 199 200 // don't use old events 201 if st.type == .music, 202 let music = pdata.status.music, 203 ev.created_at < music.created_at { 204 return 205 } else if st.type == .general, 206 let general = pdata.status.general, 207 ev.created_at < general.created_at { 208 return 209 } 210 211 pdata.status.update_status(st) 212 } 213 214 func handle_nwc_response(_ ev: NostrEvent, relay: RelayURL) { 215 Task { @MainActor in 216 // TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time 217 guard let nwc_str = damus_state.settings.nostr_wallet_connect, 218 let nwc = WalletConnectURL(str: nwc_str), 219 let resp = await FullWalletResponse(from: ev, nwc: nwc) else { 220 return 221 } 222 223 // since command results are not returned for ephemeral events, 224 // remove the request from the postbox which is likely failing over and over 225 if damus_state.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) { 226 print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]") 227 } else { 228 print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]") 229 } 230 231 guard resp.response.error == nil else { 232 print("nwc error: \(resp.response)") 233 nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp) 234 return 235 } 236 237 print("nwc success: \(resp.response.result.debugDescription) [\(relay)]") 238 nwc_success(state: self.damus_state, resp: resp) 239 } 240 } 241 242 @MainActor 243 func handle_zap_event(_ ev: NostrEvent) { 244 process_zap_event(state: damus_state, ev: ev) { zapres in 245 guard case .done(let zap) = zapres, 246 zap.target.pubkey == self.damus_state.keypair.pubkey, 247 should_show_event(state: self.damus_state, ev: zap.request.ev) else { 248 return 249 } 250 251 if !self.notifications.insert_zap(.zap(zap)) { 252 return 253 } 254 255 guard let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: .notifications, shouldNotify: true) else { 256 return 257 } 258 259 if self.damus_state.settings.zap_vibration { 260 // Generate zap vibration 261 zap_vibrate(zap_amount: zap.invoice.amount) 262 } 263 264 if self.damus_state.settings.zap_notification { 265 // Create in-app local notification for zap received. 266 switch zap.target { 267 case .profile(let profile_id): 268 create_in_app_profile_zap_notification(profiles: self.damus_state.profiles, zap: zap, profile_id: profile_id) 269 case .note(let note_target): 270 create_in_app_event_zap_notification(profiles: self.damus_state.profiles, zap: zap, evId: note_target.note_id) 271 } 272 } 273 274 self.notification_status.new_events = new_bits 275 } 276 277 } 278 279 @MainActor 280 func handle_damus_app_notification(_ notification: DamusAppNotification) async { 281 if self.notifications.insert_app_notification(notification: notification) { 282 // If we successfully inserted a new Damus App notification, switch ON the Damus App notification bit on our NewsEventsBits 283 // This will cause the bell icon on the tab bar to display the purple dot indicating there is an unread notification 284 self.notification_status.new_events = NewEventsBits(rawValue: self.notification_status.new_events.rawValue | NewEventsBits.damus_app_notifications.rawValue) 285 } 286 } 287 288 func filter_events() { 289 events.filter { ev in 290 !damus_state.mutelist_manager.is_muted(.user(ev.pubkey, nil)) 291 } 292 293 self.dms.dms = dms.dms.filter { ev in 294 !damus_state.mutelist_manager.is_muted(.user(ev.pubkey, nil)) 295 } 296 297 notifications.filter { ev in 298 if damus_state.settings.onlyzaps_mode && ev.known_kind == NostrKind.like { 299 return false 300 } 301 302 let event_muted = damus_state.mutelist_manager.is_event_muted(ev) 303 return !event_muted 304 } 305 } 306 307 func handle_delete_event(_ ev: NostrEvent) { 308 self.deleted_events.insert(ev.id) 309 } 310 311 func handle_contact_event(sub_id: String, relay_id: RelayURL, ev: NostrEvent) { 312 process_contact_event(state: self.damus_state, ev: ev) 313 314 if sub_id == init_subid { 315 pool.send(.unsubscribe(init_subid), to: [relay_id]) 316 if !done_init { 317 done_init = true 318 send_home_filters(relay_id: nil) 319 } 320 } 321 } 322 323 func handle_boost_event(sub_id: String, _ ev: NostrEvent) { 324 var boost_ev_id = ev.last_refid() 325 326 if let inner_ev = ev.get_inner_event(cache: damus_state.events) { 327 boost_ev_id = inner_ev.id 328 329 Task { 330 guard validate_event(ev: inner_ev) == .ok else { 331 return 332 } 333 334 if inner_ev.is_textlike { 335 DispatchQueue.main.async { 336 self.handle_text_event(sub_id: sub_id, ev) 337 } 338 } 339 } 340 } 341 342 guard let e = boost_ev_id else { 343 return 344 } 345 346 switch self.damus_state.boosts.add_event(ev, target: e) { 347 case .already_counted: 348 break 349 case .success(let n): 350 notify(.update_stats(note_id: e)) 351 } 352 } 353 354 func handle_quote_repost_event(_ ev: NostrEvent, target: NoteId) { 355 switch damus_state.quote_reposts.add_event(ev, target: target) { 356 case .already_counted: 357 break 358 case .success(let n): 359 notify(.update_stats(note_id: target)) 360 } 361 } 362 363 func handle_like_event(_ ev: NostrEvent) { 364 guard let e = ev.last_refid() else { 365 // no id ref? invalid like event 366 return 367 } 368 369 if damus_state.settings.onlyzaps_mode { 370 return 371 } 372 373 switch damus_state.likes.add_event(ev, target: e) { 374 case .already_counted: 375 break 376 case .success(let n): 377 handle_notification(ev: ev) 378 let liked = Counted(event: ev, id: e, total: n) 379 notify(.liked(liked)) 380 notify(.update_stats(note_id: e)) 381 } 382 } 383 384 @MainActor 385 func handle_event(relay_id: RelayURL, conn_event: NostrConnectionEvent) { 386 switch conn_event { 387 case .ws_event(let ev): 388 switch ev { 389 case .connected: 390 if !done_init { 391 self.loading = true 392 send_initial_filters(relay_id: relay_id) 393 } else { 394 //remove_bootstrap_nodes(damus_state) 395 send_home_filters(relay_id: relay_id) 396 } 397 398 // connect to nwc relays when connected 399 if let nwc_str = damus_state.settings.nostr_wallet_connect, 400 let r = pool.get_relay(relay_id), 401 r.descriptor.variant == .nwc, 402 let nwc = WalletConnectURL(str: nwc_str), 403 nwc.relay == relay_id 404 { 405 subscribe_to_nwc(url: nwc, pool: pool) 406 } 407 case .error(let merr): 408 let desc = String(describing: merr) 409 if desc.contains("Software caused connection abort") { 410 pool.reconnect(to: [relay_id]) 411 } 412 case .disconnected: 413 pool.reconnect(to: [relay_id]) 414 default: 415 break 416 } 417 418 update_signal_from_pool(signal: self.signal, pool: damus_state.pool) 419 case .nostr_event(let ev): 420 switch ev { 421 case .event(let sub_id, let ev): 422 // globally handle likes 423 /* 424 let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .boost || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata 425 if !always_process { 426 // TODO: other views like threads might have their own sub ids, so ignore those events... or should we? 427 return 428 } 429 */ 430 431 self.process_event(sub_id: sub_id, relay_id: relay_id, ev: ev) 432 case .notice(let msg): 433 print(msg) 434 435 case .eose(let sub_id): 436 guard let txn = NdbTxn(ndb: damus_state.ndb) else { 437 return 438 } 439 440 if sub_id == dms_subid { 441 var dms = dms.dms.flatMap { $0.events } 442 dms.append(contentsOf: incoming_dms) 443 load_profiles(context: "dms", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state, txn: txn) 444 } else if sub_id == notifications_subid { 445 load_profiles(context: "notifications", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state, txn: txn) 446 } else if sub_id == home_subid { 447 load_profiles(context: "home", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus_state, txn: txn) 448 } 449 450 self.loading = false 451 break 452 453 case .ok: 454 break 455 case .auth: 456 break 457 } 458 459 } 460 } 461 462 463 /// Send the initial filters, just our contact list mostly 464 func send_initial_filters(relay_id: RelayURL) { 465 let filter = NostrFilter(kinds: [.contacts], limit: 1, authors: [damus_state.pubkey]) 466 let subscription = NostrSubscribe(filters: [filter], sub_id: init_subid) 467 pool.send(.subscribe(subscription), to: [relay_id]) 468 } 469 470 /// After initial connection or reconnect, send subscription filters for the home timeline, DMs, and notifications 471 func send_home_filters(relay_id: RelayURL?) { 472 // TODO: since times should be based on events from a specific relay 473 // perhaps we could mark this in the relay pool somehow 474 475 let friends = get_friends() 476 477 var contacts_filter = NostrFilter(kinds: [.metadata]) 478 contacts_filter.authors = friends 479 480 var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata]) 481 our_contacts_filter.authors = [damus_state.pubkey] 482 483 var our_old_blocklist_filter = NostrFilter(kinds: [.list_deprecated]) 484 our_old_blocklist_filter.parameter = ["mute"] 485 our_old_blocklist_filter.authors = [damus_state.pubkey] 486 487 var our_blocklist_filter = NostrFilter(kinds: [.mute_list]) 488 our_blocklist_filter.authors = [damus_state.pubkey] 489 490 var dms_filter = NostrFilter(kinds: [.dm]) 491 492 var our_dms_filter = NostrFilter(kinds: [.dm]) 493 494 // friends only?... 495 //dms_filter.authors = friends 496 dms_filter.limit = 500 497 dms_filter.pubkeys = [ damus_state.pubkey ] 498 our_dms_filter.authors = [ damus_state.pubkey ] 499 500 var notifications_filter_kinds: [NostrKind] = [ 501 .text, 502 .boost, 503 .zap, 504 ] 505 if !damus_state.settings.onlyzaps_mode { 506 notifications_filter_kinds.append(.like) 507 } 508 var notifications_filter = NostrFilter(kinds: notifications_filter_kinds) 509 notifications_filter.pubkeys = [damus_state.pubkey] 510 notifications_filter.limit = 500 511 512 var notifications_filters = [notifications_filter] 513 var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter] 514 var dms_filters = [dms_filter, our_dms_filter] 515 let last_of_kind = get_last_of_kind(relay_id: relay_id) 516 517 contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters) 518 notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters) 519 dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters) 520 521 //print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters]) 522 523 subscribe_to_home_filters(relay_id: relay_id) 524 525 let relay_ids = relay_id.map { [$0] } 526 527 pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: relay_ids) 528 pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: relay_ids) 529 pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: relay_ids) 530 } 531 532 func get_last_of_kind(relay_id: RelayURL?) -> [UInt32: NostrEvent] { 533 return relay_id.flatMap { last_event_of_kind[$0] } ?? [:] 534 } 535 536 func unsubscribe_to_home_filters() { 537 pool.send(.unsubscribe(home_subid)) 538 } 539 540 func get_friends() -> [Pubkey] { 541 var friends = damus_state.contacts.get_friend_list() 542 friends.insert(damus_state.pubkey) 543 return Array(friends) 544 } 545 546 func subscribe_to_home_filters(friends fs: [Pubkey]? = nil, relay_id: RelayURL? = nil) { 547 // TODO: separate likes? 548 var home_filter_kinds: [NostrKind] = [ 549 .text, .longform, .boost 550 ] 551 if !damus_state.settings.onlyzaps_mode { 552 home_filter_kinds.append(.like) 553 } 554 555 // only pull status data if we care for it 556 if damus_state.settings.show_music_statuses || damus_state.settings.show_general_statuses { 557 home_filter_kinds.append(.status) 558 } 559 560 let friends = fs ?? get_friends() 561 var home_filter = NostrFilter(kinds: home_filter_kinds) 562 // include our pubkey as well even if we're not technically a friend 563 home_filter.authors = friends 564 home_filter.limit = 500 565 566 var home_filters = [home_filter] 567 568 let followed_hashtags = Array(damus_state.contacts.get_followed_hashtags()) 569 if followed_hashtags.count != 0 { 570 var hashtag_filter = NostrFilter.filter_hashtag(followed_hashtags) 571 hashtag_filter.limit = 100 572 home_filters.append(hashtag_filter) 573 } 574 575 let relay_ids = relay_id.map { [$0] } 576 home_filters = update_filters_with_since(last_of_kind: get_last_of_kind(relay_id: relay_id), filters: home_filters) 577 let sub = NostrSubscribe(filters: home_filters, sub_id: home_subid) 578 579 pool.send(.subscribe(sub), to: relay_ids) 580 } 581 582 func handle_mute_list_event(_ ev: NostrEvent) { 583 // we only care about our mutelist 584 guard ev.pubkey == damus_state.pubkey else { 585 return 586 } 587 588 // we only care about the most recent mutelist 589 if let mutelist = damus_state.mutelist_manager.event { 590 if ev.created_at <= mutelist.created_at { 591 return 592 } 593 } 594 595 damus_state.mutelist_manager.set_mutelist(ev) 596 597 migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state) 598 } 599 600 func handle_old_list_event(_ ev: NostrEvent) { 601 // we only care about our lists 602 guard ev.pubkey == damus_state.pubkey else { 603 return 604 } 605 606 // we only care about the most recent mutelist 607 if let mutelist = damus_state.mutelist_manager.event { 608 if ev.created_at <= mutelist.created_at { 609 return 610 } 611 } 612 613 guard ev.referenced_params.contains(where: { p in p.param.matches_str("mute") }) else { 614 return 615 } 616 617 damus_state.mutelist_manager.set_mutelist(ev) 618 619 migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state) 620 } 621 622 func get_last_event_of_kind(relay_id: RelayURL, kind: UInt32) -> NostrEvent? { 623 guard let m = last_event_of_kind[relay_id] else { 624 last_event_of_kind[relay_id] = [:] 625 return nil 626 } 627 628 return m[kind] 629 } 630 631 func handle_notification(ev: NostrEvent) { 632 // don't show notifications from ourselves 633 guard ev.pubkey != damus_state.pubkey, 634 event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey), 635 should_show_event(state: damus_state, ev: ev) else { 636 return 637 } 638 639 damus_state.events.insert(ev) 640 641 if let inner_ev = ev.get_inner_event(cache: damus_state.events) { 642 damus_state.events.insert(inner_ev) 643 } 644 645 if !notifications.insert_event(ev, damus_state: damus_state) { 646 return 647 } 648 649 if handle_last_event(ev: ev, timeline: .notifications) { 650 process_local_notification(state: damus_state, event: ev) 651 } 652 653 } 654 655 @discardableResult 656 func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool { 657 if let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) { 658 self.notification_status.new_events = new_bits 659 return true 660 } else { 661 return false 662 } 663 } 664 665 func insert_home_event(_ ev: NostrEvent) { 666 if events.insert(ev) { 667 handle_last_event(ev: ev, timeline: .home) 668 } 669 } 670 671 672 func handle_text_event(sub_id: String, _ ev: NostrEvent) { 673 guard should_show_event(state: damus_state, ev: ev) else { 674 return 675 } 676 677 // TODO: will we need to process this in other places like zap request contents, etc? 678 process_image_metadatas(cache: damus_state.events, ev: ev) 679 damus_state.replies.count_replies(ev, keypair: self.damus_state.keypair) 680 damus_state.events.insert(ev) 681 682 if let quoted_event = ev.referenced_quote_ids.first { 683 handle_quote_repost_event(ev, target: quoted_event.note_id) 684 } 685 686 if sub_id == home_subid { 687 insert_home_event(ev) 688 } else if sub_id == notifications_subid { 689 handle_notification(ev: ev) 690 } 691 } 692 693 func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) { 694 notification_status.new_events = notifs 695 696 guard should_display_notification(state: damus_state, event: ev), 697 let notification_object = generate_local_notification_object(from: ev, state: damus_state) 698 else { 699 return 700 } 701 702 create_local_notification(profiles: damus_state.profiles, notify: notification_object) 703 } 704 705 func handle_dm(_ ev: NostrEvent) { 706 guard should_show_event(state: damus_state, ev: ev) else { 707 return 708 } 709 710 damus_state.events.insert(ev) 711 712 if !should_debounce_dms { 713 self.incoming_dms.append(ev) 714 if let notifs = handle_incoming_dms(prev_events: notification_status.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) { 715 got_new_dm(notifs: notifs, ev: ev) 716 } 717 self.incoming_dms = [] 718 return 719 } 720 721 incoming_dms.append(ev) 722 723 dm_debouncer.debounce { [self] in 724 if let notifs = handle_incoming_dms(prev_events: notification_status.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) { 725 got_new_dm(notifs: notifs, ev: ev) 726 } 727 self.incoming_dms = [] 728 } 729 } 730 } 731 732 733 func update_signal_from_pool(signal: SignalModel, pool: RelayPool) { 734 if signal.max_signal != pool.relays.count { 735 signal.max_signal = pool.relays.count 736 } 737 738 if signal.signal != pool.num_connected { 739 signal.signal = pool.num_connected 740 } 741 } 742 743 func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) { 744 if !contacts.is_friend(ev.pubkey) { 745 return 746 } 747 748 contacts.add_friend_contact(ev) 749 } 750 751 func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) { 752 let contacts = state.contacts 753 let new_refs = Set<FollowRef>(ev.referenced_follows) 754 let old_refs = m_old_ev.map({ old_ev in Set(old_ev.referenced_follows) }) ?? Set() 755 756 let diff = new_refs.symmetricDifference(old_refs) 757 for ref in diff { 758 if new_refs.contains(ref) { 759 notify(.followed(ref)) 760 switch ref { 761 case .pubkey(let pk): 762 contacts.add_friend_pubkey(pk) 763 case .hashtag: 764 // I guess I could cache followed hashtags here... whatever 765 break 766 } 767 } else { 768 notify(.unfollowed(ref)) 769 switch ref { 770 case .pubkey(let pk): 771 contacts.remove_friend(pk) 772 case .hashtag: break 773 } 774 } 775 } 776 } 777 778 779 func abbrev_ids(_ ids: [String]) -> String { 780 if ids.count > 5 { 781 let n = ids.count - 5 782 return "[" + ids[..<5].joined(separator: ",") + ", ... (\(n) more)]" 783 } 784 return "\(ids)" 785 } 786 787 func abbrev_field<T: CustomStringConvertible>(_ n: String, _ field: T?) -> String { 788 guard let field = field else { 789 return "" 790 } 791 792 return "\(n):\(field.description)" 793 } 794 795 func abbrev_ids_field(_ n: String, _ ids: [String]?) -> String { 796 guard let ids = ids else { 797 return "" 798 } 799 800 return "\(n): \(abbrev_ids(ids))" 801 } 802 803 /* 804 func print_filter(_ f: NostrFilter) { 805 let fmt = [ 806 abbrev_ids_field("ids", f.ids), 807 abbrev_field("kinds", f.kinds), 808 abbrev_ids_field("authors", f.authors), 809 abbrev_ids_field("referenced_ids", f.referenced_ids), 810 abbrev_ids_field("pubkeys", f.pubkeys), 811 abbrev_field("since", f.since), 812 abbrev_field("until", f.until), 813 abbrev_field("limit", f.limit) 814 ].filter({ !$0.isEmpty }).joined(separator: ",") 815 816 print("Filter(\(fmt))") 817 } 818 819 func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) { 820 let relays = relay_id ?? "relays" 821 print("connected to \(relays) with filters:") 822 for group in groups { 823 for filter in group { 824 print_filter(filter) 825 } 826 } 827 print("-----") 828 } 829 */ 830 831 // TODO: remove this, let nostrdb handle all validation 832 func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) { 833 let validated = events.is_event_valid(ev.id) 834 835 switch validated { 836 case .unknown: 837 Task.detached(priority: .medium) { 838 let result = validate_event(ev: ev) 839 840 DispatchQueue.main.async { 841 events.store_event_validation(evid: ev.id, validated: result) 842 guard result == .ok else { 843 return 844 } 845 callback() 846 } 847 } 848 849 case .ok: 850 callback() 851 852 case .bad_id, .bad_sig: 853 break 854 } 855 } 856 857 func robohash(_ pk: Pubkey) -> String { 858 return "https://robohash.org/" + pk.hex() 859 } 860 861 func load_our_stuff(state: DamusState, ev: NostrEvent) { 862 guard ev.pubkey == state.pubkey else { 863 return 864 } 865 866 // only use new stuff 867 if let current_ev = state.contacts.event { 868 guard ev.created_at > current_ev.created_at else { 869 return 870 } 871 } 872 873 let m_old_ev = state.contacts.event 874 state.contacts.event = ev 875 876 load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev) 877 load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev) 878 } 879 880 func process_contact_event(state: DamusState, ev: NostrEvent) { 881 load_our_stuff(state: state, ev: ev) 882 add_contact_if_friend(contacts: state.contacts, ev: ev) 883 } 884 885 func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) { 886 let bootstrap_dict: [RelayURL: RelayInfo] = [:] 887 let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? state.bootstrap_relays.reduce(into: bootstrap_dict) { (d, r) in 888 d[r] = .rw 889 } 890 891 guard let decoded: [RelayURL: RelayInfo] = decode_json_relays(ev.content) else { 892 return 893 } 894 895 var changed = false 896 897 var new = Set<RelayURL>() 898 for key in decoded.keys { 899 new.insert(key) 900 } 901 902 var old = Set<RelayURL>() 903 for key in old_decoded.keys { 904 old.insert(key) 905 } 906 907 let diff = old.symmetricDifference(new) 908 909 let new_relay_filters = load_relay_filters(state.pubkey) == nil 910 for d in diff { 911 changed = true 912 if new.contains(d) { 913 let descriptor = RelayDescriptor(url: d, info: decoded[d] ?? .rw) 914 add_new_relay(model_cache: state.relay_model_cache, relay_filters: state.relay_filters, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: state.settings.developer_mode) 915 } else { 916 state.pool.remove_relay(d) 917 } 918 } 919 920 if changed { 921 save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new)) 922 state.pool.connect() 923 notify(.relays_changed) 924 } 925 } 926 927 func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) { 928 try? pool.add_relay(descriptor) 929 let url = descriptor.url 930 931 let relay_id = url 932 guard model_cache.model(withURL: url) == nil else { 933 return 934 } 935 936 Task.detached(priority: .background) { 937 guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else { 938 return 939 } 940 941 await MainActor.run { 942 let model = RelayModel(url, metadata: meta) 943 model_cache.insert(model: model) 944 945 if logging_enabled { 946 pool.setLog(model.log, for: relay_id) 947 } 948 949 // if this is the first time adding filters, we should filter non-paid relays 950 if new_relay_filters && !meta.is_paid { 951 relay_filters.insert(timeline: .search, relay_id: relay_id) 952 } 953 } 954 } 955 } 956 957 func fetch_relay_metadata(relay_id: RelayURL) async throws -> RelayMetadata? { 958 var urlString = relay_id.absoluteString.replacingOccurrences(of: "wss://", with: "https://") 959 urlString = urlString.replacingOccurrences(of: "ws://", with: "http://") 960 961 guard let url = URL(string: urlString) else { 962 return nil 963 } 964 965 var request = URLRequest(url: url) 966 request.setValue("application/nostr+json", forHTTPHeaderField: "Accept") 967 968 var res: (Data, URLResponse)? = nil 969 970 res = try await URLSession.shared.data(for: request) 971 972 guard let data = res?.0 else { 973 return nil 974 } 975 976 let nip11 = try JSONDecoder().decode(RelayMetadata.self, from: data) 977 return nip11 978 } 979 980 @discardableResult 981 func handle_incoming_dm(ev: NostrEvent, our_pubkey: Pubkey, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) { 982 var inserted = false 983 var found = false 984 985 let ours = ev.pubkey == our_pubkey 986 var i = 0 987 988 var the_pk = ev.pubkey 989 if ours { 990 if let ref_pk = ev.referenced_pubkeys.first { 991 the_pk = ref_pk 992 } else { 993 // self dm!? 994 print("TODO: handle self dm?") 995 } 996 } 997 998 for model in dms.dms { 999 if model.pubkey == the_pk { 1000 found = true 1001 inserted = insert_uniq_sorted_event(events: &(dms.dms[i].events), new_ev: ev) { 1002 $0.created_at < $1.created_at 1003 } 1004 1005 break 1006 } 1007 i += 1 1008 } 1009 1010 if !found { 1011 let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey, pubkey: the_pk) 1012 dms.dms.append(model) 1013 inserted = true 1014 } 1015 1016 var new_bits: NewEventsBits? = nil 1017 if inserted { 1018 new_bits = handle_last_events(new_events: prev_events, ev: ev, timeline: .dms, shouldNotify: !ours) 1019 } 1020 1021 return (inserted, new_bits) 1022 } 1023 1024 @discardableResult 1025 func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: Pubkey, evs: [NostrEvent]) -> NewEventsBits? { 1026 var inserted = false 1027 1028 var new_events: NewEventsBits? = nil 1029 1030 for ev in evs { 1031 let res = handle_incoming_dm(ev: ev, our_pubkey: our_pubkey, dms: dms, prev_events: prev_events) 1032 inserted = res.0 || inserted 1033 if let new = res.1 { 1034 new_events = new 1035 } 1036 } 1037 1038 if inserted { 1039 let new_dms = Array(dms.dms.filter({ $0.events.count > 0 })).sorted { a, b in 1040 return a.events.last!.created_at > b.events.last!.created_at 1041 } 1042 1043 dms.dms = new_dms 1044 } 1045 1046 return new_events 1047 } 1048 1049 func determine_event_notifications(_ ev: NostrEvent) -> NewEventsBits { 1050 guard let kind = ev.known_kind else { 1051 return [] 1052 } 1053 1054 if kind == .zap { 1055 return [.zaps] 1056 } 1057 1058 if kind == .boost { 1059 return [.reposts] 1060 } 1061 1062 if kind == .text { 1063 return [.mentions] 1064 } 1065 1066 if kind == .like { 1067 return [.likes] 1068 } 1069 1070 return [] 1071 } 1072 1073 func timeline_to_notification_bits(_ timeline: Timeline, ev: NostrEvent?) -> NewEventsBits { 1074 switch timeline { 1075 case .home: 1076 return [.home] 1077 case .notifications: 1078 if let ev { 1079 return determine_event_notifications(ev) 1080 } 1081 return [.notifications] 1082 case .search: 1083 return [.search] 1084 case .dms: 1085 return [.dms] 1086 } 1087 } 1088 1089 /// A helper to determine if we need to notify the user of new events 1090 func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> NewEventsBits? { 1091 let last_ev = get_last_event(timeline) 1092 1093 if last_ev == nil || last_ev!.created_at < ev.created_at { 1094 save_last_event(ev, timeline: timeline) 1095 if shouldNotify { 1096 return new_events.union(timeline_to_notification_bits(timeline, ev: ev)) 1097 } 1098 } 1099 1100 return nil 1101 } 1102 1103 1104 /// Sometimes we get garbage in our notifications. Ensure we have our pubkey on this event 1105 func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: Pubkey) -> Bool { 1106 return ev.referenced_pubkeys.contains(our_pubkey) 1107 } 1108 1109 func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool { 1110 return should_show_event( 1111 state: damus_state, 1112 ev: event 1113 ) 1114 } 1115 1116 func should_show_event(state: DamusState, ev: NostrEvent, keypair: Keypair? = nil) -> Bool { 1117 let event_muted = state.mutelist_manager.is_event_muted(ev, keypair: keypair) 1118 if event_muted { 1119 return false 1120 } 1121 1122 return ev.should_show_event 1123 } 1124 1125 func zap_vibrate(zap_amount: Int64) { 1126 let sats = zap_amount / 1000 1127 var vibration_generator: UIImpactFeedbackGenerator 1128 if sats >= 10000 { 1129 vibration_generator = UIImpactFeedbackGenerator(style: .heavy) 1130 } else if sats >= 1000 { 1131 vibration_generator = UIImpactFeedbackGenerator(style: .medium) 1132 } else { 1133 vibration_generator = UIImpactFeedbackGenerator(style: .light) 1134 } 1135 vibration_generator.impactOccurred() 1136 } 1137 1138 func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, profile_id: Pubkey) { 1139 let content = UNMutableNotificationContent() 1140 1141 content.title = NotificationFormatter.zap_notification_title(zap) 1142 content.body = NotificationFormatter.zap_notification_body(profiles: profiles, zap: zap, locale: locale) 1143 content.sound = UNNotificationSound.default 1144 content.userInfo = LossyLocalNotification(type: .profile_zap, mention: .pubkey(profile_id)).to_user_info() 1145 1146 let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) 1147 1148 let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger) 1149 1150 UNUserNotificationCenter.current().add(request) { error in 1151 if let error = error { 1152 print("Error: \(error)") 1153 } else { 1154 print("Local notification scheduled") 1155 } 1156 } 1157 } 1158 1159 func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: NoteId) { 1160 let content = UNMutableNotificationContent() 1161 1162 content.title = NotificationFormatter.zap_notification_title(zap) 1163 content.body = NotificationFormatter.zap_notification_body(profiles: profiles, zap: zap, locale: locale) 1164 content.sound = UNNotificationSound.default 1165 content.userInfo = LossyLocalNotification(type: .zap, mention: .note(evId)).to_user_info() 1166 1167 let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) 1168 1169 let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger) 1170 1171 UNUserNotificationCenter.current().add(request) { error in 1172 if let error = error { 1173 print("Error: \(error)") 1174 } else { 1175 print("Local notification scheduled") 1176 } 1177 } 1178 }