NotificationsManager.swift (8967B)
1 // 2 // NotificationsManager.swift 3 // damus 4 // 5 // Handles several aspects of notification logic (Both local and push notifications) 6 // 7 // Created by Daniel D’Aquino on 2023-11-24. 8 // 9 10 import Foundation 11 import UIKit 12 13 let EVENT_MAX_AGE_FOR_NOTIFICATION: TimeInterval = 12 * 60 * 60 14 15 func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent) { 16 guard should_display_notification(state: state, event: ev) else { 17 // We should not display notification. Exit. 18 return 19 } 20 21 guard let local_notification = generate_local_notification_object(from: ev, state: state) else { 22 return 23 } 24 25 create_local_notification(profiles: state.profiles, notify: local_notification) 26 } 27 28 func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent) -> Bool { 29 if ev.known_kind == nil { 30 return false 31 } 32 33 if state.settings.notification_only_from_following, 34 state.contacts.follow_state(ev.pubkey) != .follows 35 { 36 return false 37 } 38 39 // Don't show notifications that match mute list. 40 if state.mutelist_manager.is_event_muted(ev) { 41 return false 42 } 43 44 // Don't show notifications for old events 45 guard ev.age < EVENT_MAX_AGE_FOR_NOTIFICATION else { 46 return false 47 } 48 49 return true 50 } 51 52 func generate_local_notification_object(from ev: NostrEvent, state: HeadlessDamusState) -> LocalNotification? { 53 guard let type = ev.known_kind else { 54 return nil 55 } 56 57 if type == .text, state.settings.mention_notification { 58 let blocks = ev.blocks(state.keypair).blocks 59 for case .mention(let mention) in blocks { 60 guard case .pubkey(let pk) = mention.ref, pk == state.keypair.pubkey else { 61 continue 62 } 63 let content_preview = render_notification_content_preview(ev: ev, profiles: state.profiles, keypair: state.keypair) 64 return LocalNotification(type: .mention, event: ev, target: ev, content: content_preview) 65 } 66 } else if type == .boost, 67 state.settings.repost_notification, 68 let inner_ev = ev.get_inner_event() 69 { 70 let content_preview = render_notification_content_preview(ev: inner_ev, profiles: state.profiles, keypair: state.keypair) 71 return LocalNotification(type: .repost, event: ev, target: inner_ev, content: content_preview) 72 } else if type == .like, 73 state.settings.like_notification, 74 let evid = ev.referenced_ids.last, 75 let txn = state.ndb.lookup_note(evid, txn_name: "local_notification_like"), 76 let liked_event = txn.unsafeUnownedValue?.to_owned() 77 { 78 let content_preview = render_notification_content_preview(ev: liked_event, profiles: state.profiles, keypair: state.keypair) 79 return LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview) 80 } 81 else if type == .dm, 82 state.settings.dm_notification { 83 let convo = ev.decrypted(keypair: state.keypair) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message") 84 return LocalNotification(type: .dm, event: ev, target: ev, content: convo) 85 } 86 else if type == .zap, 87 state.settings.zap_notification { 88 return LocalNotification(type: .zap, event: ev, target: ev, content: ev.content) 89 } 90 91 return nil 92 } 93 94 func create_local_notification(profiles: Profiles, notify: LocalNotification) { 95 let displayName = event_author_name(profiles: profiles, pubkey: notify.event.pubkey) 96 97 guard let (content, identifier) = NotificationFormatter.shared.format_message(displayName: displayName, notify: notify) else { return } 98 99 let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) 100 101 let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) 102 103 UNUserNotificationCenter.current().add(request) { error in 104 if let error = error { 105 print("Error: \(error)") 106 } else { 107 print("Local notification scheduled") 108 } 109 } 110 } 111 112 func render_notification_content_preview(ev: NostrEvent, profiles: Profiles, keypair: Keypair) -> String { 113 114 let prefix_len = 300 115 let artifacts = render_note_content(ev: ev, profiles: profiles, keypair: keypair) 116 117 // special case for longform events 118 if ev.known_kind == .longform { 119 let longform = LongformEvent(event: ev) 120 return longform.title ?? longform.summary ?? "Longform Event" 121 } 122 123 switch artifacts { 124 case .longform: 125 // we should never hit this until we have more note types built out of parts 126 // since we handle this case above in known_kind == .longform 127 return String(ev.content.prefix(prefix_len)) 128 129 case .separated(let artifacts): 130 return String(NSAttributedString(artifacts.content.attributed).string.prefix(prefix_len)) 131 } 132 } 133 134 func event_author_name(profiles: Profiles, pubkey: Pubkey) -> String { 135 let profile_txn = profiles.lookup(id: pubkey) 136 let profile = profile_txn?.unsafeUnownedValue 137 return Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) 138 } 139 140 @MainActor 141 func get_zap(from ev: NostrEvent, state: HeadlessDamusState) async -> Zap? { 142 return await withCheckedContinuation { continuation in 143 process_zap_event(state: state, ev: ev) { zapres in 144 continuation.resume(returning: zapres.get_zap()) 145 } 146 } 147 } 148 149 @MainActor 150 func process_zap_event(state: HeadlessDamusState, ev: NostrEvent, completion: @escaping (ProcessZapResult) -> Void) { 151 // These are zap notifications 152 guard let ptag = get_zap_target_pubkey(ev: ev, ndb: state.ndb) else { 153 completion(.failed) 154 return 155 } 156 157 // just return the zap if we already have it 158 if let zap = state.zaps.zaps[ev.id], case .zap(let z) = zap { 159 completion(.already_processed(z)) 160 return 161 } 162 163 if let local_zapper = state.profiles.lookup_zapper(pubkey: ptag) { 164 guard let zap = process_zap_event_with_zapper(state: state, ev: ev, zapper: local_zapper) else { 165 completion(.failed) 166 return 167 } 168 state.add_zap(zap: .zap(zap)) 169 completion(.done(zap)) 170 return 171 } 172 173 guard let txn = state.profiles.lookup_with_timestamp(ptag), 174 let lnurl = txn.map({ pr in pr?.lnurl }).value else { 175 completion(.failed) 176 return 177 } 178 179 Task { [lnurl] in 180 guard let zapper = await fetch_zapper_from_lnurl(lnurls: state.lnurls, pubkey: ptag, lnurl: lnurl) else { 181 completion(.failed) 182 return 183 } 184 185 DispatchQueue.main.async { 186 state.profiles.profile_data(ptag).zapper = zapper 187 guard let zap = process_zap_event_with_zapper(state: state, ev: ev, zapper: zapper) else { 188 completion(.failed) 189 return 190 } 191 state.add_zap(zap: .zap(zap)) 192 completion(.done(zap)) 193 } 194 } 195 } 196 197 // securely get the zap target's pubkey. this can be faked so we need to be 198 // careful 199 func get_zap_target_pubkey(ev: NostrEvent, ndb: Ndb) -> Pubkey? { 200 let etags = Array(ev.referenced_ids) 201 202 guard let etag = etags.first else { 203 // no etags, ptag-only case 204 205 guard let a = ev.referenced_pubkeys.just_one() else { 206 return nil 207 } 208 209 // TODO: just return data here 210 return a 211 } 212 213 // we have an e-tag 214 215 // ensure that there is only 1 etag to stop fake note zap attacks 216 guard etags.count == 1 else { 217 return nil 218 } 219 220 // we can't trust the p tag on note zaps because they can be faked 221 guard let txn = ndb.lookup_note(etag), 222 let pk = txn.unsafeUnownedValue?.pubkey else { 223 // We don't have the event in cache so we can't check the pubkey. 224 225 // We could return this as an invalid zap but that wouldn't be correct 226 // all of the time, and may reject valid zaps. What we need is a new 227 // unvalidated zap state, but for now we simply leak a bit of correctness... 228 229 return ev.referenced_pubkeys.just_one() 230 } 231 232 return pk 233 } 234 235 fileprivate func process_zap_event_with_zapper(state: HeadlessDamusState, ev: NostrEvent, zapper: Pubkey) -> Zap? { 236 let our_keypair = state.keypair 237 238 guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else { 239 return nil 240 } 241 242 state.add_zap(zap: .zap(zap)) 243 244 return zap 245 } 246 247 enum ProcessZapResult { 248 case already_processed(Zap) 249 case done(Zap) 250 case failed 251 252 func get_zap() -> Zap? { 253 switch self { 254 case .already_processed(let zap): 255 return zap 256 case .done(let zap): 257 return zap 258 default: 259 return nil 260 } 261 } 262 }