NostrEvent.swift (23502B)
1 // 2 // NostrEvent.swift 3 // damus 4 // 5 // Created by William Casarin on 2022-04-11. 6 // 7 8 import Foundation 9 import CommonCrypto 10 import secp256k1 11 import secp256k1_implementation 12 import CryptoKit 13 import NaturalLanguage 14 15 16 enum ValidationResult: Decodable { 17 case unknown 18 case ok 19 case bad_id 20 case bad_sig 21 } 22 23 /* 24 class NostrEventOld: Codable, Identifiable, CustomStringConvertible, Equatable, Hashable, Comparable { 25 // TODO: memory mapped db events 26 private var note_data: UnsafeMutablePointer<ndb_note> 27 28 init(data: UnsafeMutablePointer<ndb_note>) { 29 self.note_data = data 30 } 31 32 var id: [UInt8] { 33 let buffer = UnsafeBufferPointer(start: ndb_note_id(note_data), count: 32) 34 return Array(buffer) 35 } 36 37 var content: String { 38 String(cString: ndb_note_content(self.note_data)) 39 } 40 41 var sig: [UInt8] { 42 let buffer = UnsafeBufferPointer(start: ndb_note_signature(note_data), count: 64) 43 return Array(buffer) 44 } 45 46 var tags: TagIterator 47 48 let id: String 49 let content: String 50 let sig: String 51 let tags: Tags 52 53 //var boosted_by: String? 54 55 // cached field for pow calc 56 //var pow: Int? 57 58 // custom flags for internal use 59 //var flags: Int = 0 60 61 let pubkey: String 62 let created_at: UInt32 63 let kind: UInt32 64 65 // cached stuff 66 private var _event_refs: [EventRef]? = nil 67 var decrypted_content: String? = nil 68 private var _blocks: Blocks? = nil 69 private lazy var inner_event: NostrEventOld? = { 70 return event_from_json(dat: self.content) 71 }() 72 73 static func == (lhs: NostrEventOld, rhs: NostrEventOld) -> Bool { 74 return lhs.id == rhs.id 75 } 76 77 static func < (lhs: NostrEventOld, rhs: NostrEventOld) -> Bool { 78 return lhs.created_at < rhs.created_at 79 } 80 81 func hash(into hasher: inout Hasher) { 82 hasher.combine(id) 83 } 84 85 private enum CodingKeys: String, CodingKey { 86 case id, sig, tags, pubkey, created_at, kind, content 87 } 88 89 static func owned_from_json(json: String) -> NostrEventOld? { 90 let decoder = JSONDecoder() 91 guard let dat = json.data(using: .utf8) else { 92 return nil 93 } 94 guard let ev = try? decoder.decode(NostrEventOld.self, from: dat) else { 95 return nil 96 } 97 98 return ev 99 } 100 101 init?(content: String, keypair: Keypair, kind: UInt32 = 1, tags: [[String]] = [], createdAt: UInt32 = UInt32(Date().timeIntervalSince1970)) { 102 103 self.content = content 104 self.pubkey = keypair.pubkey 105 self.kind = kind 106 self.tags = tags 107 self.created_at = createdAt 108 109 if let privkey = keypair.privkey { 110 self.id = hex_encode(calculate_event_id(pubkey: pubkey, created_at: created_at, kind: kind, tags: tags, content: content)) 111 self.sig = sign_id(privkey: privkey, id: self.id) 112 } else { 113 self.id = "" 114 self.sig = "" 115 } 116 } 117 } 118 119 extension NostrEventOld { 120 var is_textlike: Bool { 121 return kind == 1 || kind == 42 || kind == 30023 122 } 123 124 var too_big: Bool { 125 return known_kind != .longform && self.content.utf8.count > 16000 126 } 127 128 var should_show_event: Bool { 129 return !too_big 130 } 131 132 func blocks(_ privkey: String?) -> Blocks { 133 if let bs = _blocks { 134 return bs 135 } 136 let blocks = get_blocks(content: self.get_content(privkey)) 137 self._blocks = blocks 138 return blocks 139 } 140 141 func get_blocks(content: String) -> Blocks { 142 return parse_note_content(content: content, tags: self.tags) 143 } 144 145 146 func get_inner_event(cache: EventCache) -> NostrEventOld? { 147 guard self.known_kind == .boost else { 148 return nil 149 } 150 151 if self.content == "", let ref = self.referenced_ids.first { 152 return cache.lookup(ref.ref_id.string()) 153 } 154 155 return self.inner_event 156 } 157 158 func event_refs(_ privkey: String?) -> [EventRef] { 159 if let rs = _event_refs { 160 return rs 161 } 162 let refs = interpret_event_refs(blocks: self.blocks(privkey).blocks, tags: self.tags) 163 self._event_refs = refs 164 return refs 165 } 166 167 168 func decrypted(privkey: String?) -> String? { 169 if let decrypted_content = decrypted_content { 170 return decrypted_content 171 } 172 173 guard let key = privkey else { 174 return nil 175 } 176 177 guard let our_pubkey = privkey_to_pubkey(privkey: key) else { 178 return nil 179 } 180 181 var pubkey = self.pubkey 182 // This is our DM, we need to use the pubkey of the person we're talking to instead 183 if our_pubkey == pubkey { 184 guard let refkey = self.referenced_pubkeys.first else { 185 return nil 186 } 187 188 pubkey = refkey.ref_id 189 } 190 191 let dec = decrypt_dm(key, pubkey: pubkey, content: self.content, encoding: .base64) 192 self.decrypted_content = dec 193 194 return dec 195 } 196 197 func get_content(_ privkey: String?) -> String { 198 if known_kind == .dm { 199 return decrypted(privkey: privkey) ?? "*failed to decrypt content*" 200 } 201 202 return content 203 } 204 205 var description: String { 206 return "NostrEvent { id: \(id) pubkey \(pubkey) kind \(kind) tags \(tags) content '\(content)' }" 207 } 208 209 var known_kind: NostrKind? { 210 return NostrKind.init(rawValue: kind) 211 } 212 213 private func get_referenced_ids(key: String) -> [ReferencedId] { 214 return damus.get_referenced_ids(tags: self.tags, key: key) 215 } 216 217 public func direct_replies(_ privkey: String?) -> [ReferencedId] { 218 return event_refs(privkey).reduce(into: []) { acc, evref in 219 if let direct_reply = evref.is_direct_reply { 220 acc.append(direct_reply) 221 } 222 } 223 } 224 225 public func thread_id(privkey: String?) -> String { 226 for ref in event_refs(privkey) { 227 if let thread_id = ref.is_thread_id { 228 return thread_id.ref_id 229 } 230 } 231 232 return self.id 233 } 234 235 public func last_refid() -> ReferencedId? { 236 var mlast: Int? = nil 237 var i: Int = 0 238 for tag in tags { 239 if tag.count >= 2 && tag[0] == "e" { 240 mlast = i 241 } 242 i += 1 243 } 244 245 guard let last = mlast else { 246 return nil 247 } 248 249 return tag_to_refid(tags[last]) 250 } 251 252 public func references(id: String, key: AsciiCharacter) -> Bool { 253 for tag in tags { 254 if tag.count >= 2 && tag[0].matches_char(key) { 255 if tag[1] == id { 256 return true 257 } 258 } 259 } 260 261 return false 262 } 263 264 func is_reply(_ privkey: String?) -> Bool { 265 return event_is_reply(self.event_refs(privkey)) 266 } 267 268 func note_language(_ privkey: String?) -> String? { 269 // Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in 270 // and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer. 271 let originalBlocks = blocks(privkey).blocks 272 let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ") 273 274 // Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate. 275 let languageRecognizer = NLLanguageRecognizer() 276 languageRecognizer.processString(originalOnlyText) 277 278 guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue else { 279 return nil 280 } 281 282 // Remove the variant component and just take the language part as translation services typically only supports the variant-less language. 283 // Moreover, speakers of one variant can generally understand other variants. 284 return localeToLanguage(locale) 285 } 286 287 public var referenced_ids: [ReferencedId] { 288 return get_referenced_ids(key: "e") 289 } 290 291 public var referenced_pubkeys: [ReferencedId] { 292 return get_referenced_ids(key: "p") 293 } 294 295 public var referenced_hashtags: [ReferencedId] { 296 return get_referenced_ids(key: "t") 297 } 298 299 var age: TimeInterval { 300 let event_date = Date(timeIntervalSince1970: TimeInterval(created_at)) 301 return Date.now.timeIntervalSince(event_date) 302 } 303 } 304 */ 305 306 func sign_id(privkey: String, id: String) -> String { 307 let priv_key_bytes = try! privkey.bytes 308 let key = try! secp256k1.Signing.PrivateKey(rawRepresentation: priv_key_bytes) 309 310 // Extra params for custom signing 311 312 var aux_rand = random_bytes(count: 64).bytes 313 var digest = try! id.bytes 314 315 // API allows for signing variable length messages 316 let signature = try! key.schnorr.signature(message: &digest, auxiliaryRand: &aux_rand) 317 318 return hex_encode(signature.rawRepresentation) 319 } 320 321 func decode_nostr_event(txt: String) -> NostrResponse? { 322 return NostrResponse.owned_from_json(json: txt) 323 } 324 325 func encode_json<T: Encodable>(_ val: T) -> String? { 326 let encoder = JSONEncoder() 327 encoder.outputFormatting = .withoutEscapingSlashes 328 return (try? encode_json_data(val)).map { String(decoding: $0, as: UTF8.self) } 329 } 330 331 func encode_json_data<T: Encodable>(_ val: T) throws -> Data { 332 let encoder = JSONEncoder() 333 encoder.outputFormatting = .withoutEscapingSlashes 334 return try encoder.encode(val) 335 } 336 337 func decode_nostr_event_json(json: String) -> NostrEvent? { 338 return NostrEvent.owned_from_json(json: json) 339 } 340 341 /* 342 func decode_nostr_event_json(json: String) -> NostrEvent? { 343 guard let json_str = json.cString(using: .utf8) else { 344 return nil 345 } 346 347 // Allocate a double pointer (pointer to pointer) for ndb_note 348 var notePtr: UnsafeMutablePointer<ndb_note>? = nil 349 350 // Create the buffer 351 var buf = [Int8](repeating: 0, count: 2<<18) 352 353 // Call the C function 354 let result = withUnsafeMutablePointer(to: ¬ePtr) { (ptr) -> Int32 in 355 return ndb_note_from_json(json_str, Int32(json_str.count), ptr, &buf, Int32(buf.count)) 356 } 357 358 guard result == 0, let note = notePtr?.pointee else { 359 return nil 360 } 361 362 return .init(data: note) 363 } 364 */ 365 366 func decode_json<T: Decodable>(_ val: String) -> T? { 367 return try? JSONDecoder().decode(T.self, from: Data(val.utf8)) 368 } 369 370 func decode_data<T: Decodable>(_ data: Data) -> T? { 371 let decoder = JSONDecoder() 372 do { 373 return try decoder.decode(T.self, from: data) 374 } catch { 375 print("decode_data failed for \(T.self): \(error)") 376 } 377 378 return nil 379 } 380 381 func event_commitment(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> String { 382 let encoder = JSONEncoder() 383 encoder.outputFormatting = .withoutEscapingSlashes 384 let str_data = try! encoder.encode(content) 385 let content = String(decoding: str_data, as: UTF8.self) 386 387 let tags_encoder = JSONEncoder() 388 tags_encoder.outputFormatting = .withoutEscapingSlashes 389 let tags_data = try! tags_encoder.encode(tags) 390 let tags = String(decoding: tags_data, as: UTF8.self) 391 392 return "[0,\"\(pubkey.hex())\",\(created_at),\(kind),\(tags),\(content)]" 393 } 394 395 func calculate_event_commitment(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> Data { 396 let target = event_commitment(pubkey: pubkey, created_at: created_at, kind: kind, tags: tags, content: content) 397 return target.data(using: .utf8)! 398 } 399 400 func calculate_event_id(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> NoteId { 401 let commitment = calculate_event_commitment(pubkey: pubkey, created_at: created_at, kind: kind, tags: tags, content: content) 402 return NoteId(sha256(commitment)) 403 } 404 405 406 func sha256(_ data: Data) -> Data { 407 var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) 408 data.withUnsafeBytes { 409 _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) 410 } 411 return Data(hash) 412 } 413 414 func hexchar(_ val: UInt8) -> UInt8 { 415 if val < 10 { 416 return 48 + val; 417 } 418 if val < 16 { 419 return 97 + val - 10; 420 } 421 assertionFailure("impossiburu") 422 return 0 423 } 424 425 func random_bytes(count: Int) -> Data { 426 var bytes = [Int8](repeating: 0, count: count) 427 guard 428 SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) == errSecSuccess 429 else { 430 fatalError("can't copy secure random data") 431 } 432 return Data(bytes: bytes, count: count) 433 } 434 435 func make_boost_event(keypair: FullKeypair, boosted: NostrEvent) -> NostrEvent? { 436 var tags = Array(boosted.referenced_pubkeys).map({ pk in pk.tag }) 437 438 tags.append(["e", boosted.id.hex(), "", "root"]) 439 tags.append(["p", boosted.pubkey.hex()]) 440 441 let content = event_to_json(ev: boosted) 442 return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 6, tags: tags) 443 } 444 445 func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = "🤙") -> NostrEvent? { 446 var tags = liked.tags.reduce(into: [[String]]()) { ts, tag in 447 guard tag.count >= 2, 448 (tag[0].matches_char("e") || tag[0].matches_char("p")) else { 449 return 450 } 451 ts.append(tag.strings()) 452 } 453 454 tags.append(["e", liked.id.hex()]) 455 tags.append(["p", liked.pubkey.hex()]) 456 457 return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 7, tags: tags) 458 } 459 460 func generate_private_keypair(our_privkey: Privkey, id: NoteId, created_at: UInt32) -> FullKeypair? { 461 let to_hash = our_privkey.hex() + id.hex() + String(created_at) 462 guard let dat = to_hash.data(using: .utf8) else { 463 return nil 464 } 465 let privkey_bytes = sha256(dat) 466 let privkey = Privkey(privkey_bytes) 467 guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil } 468 469 return FullKeypair(pubkey: pubkey, privkey: privkey) 470 } 471 472 func uniq<T: Hashable>(_ xs: [T]) -> [T] { 473 var s = Set<T>() 474 var ys: [T] = [] 475 476 for x in xs { 477 if s.contains(x) { 478 continue 479 } 480 s.insert(x) 481 ys.append(x) 482 } 483 484 return ys 485 } 486 487 func gather_reply_ids(our_pubkey: Pubkey, from: NostrEvent) -> [RefId] { 488 var ids: [RefId] = from.referenced_ids.first.map({ ref in [ .event(ref) ] }) ?? [] 489 490 let pks = from.referenced_pubkeys.reduce(into: [RefId]()) { rs, pk in 491 if pk == our_pubkey { 492 return 493 } 494 rs.append(.pubkey(pk)) 495 } 496 497 ids.append(.event(from.id)) 498 ids.append(contentsOf: uniq(pks)) 499 500 if from.pubkey != our_pubkey { 501 ids.append(.pubkey(from.pubkey)) 502 } 503 504 return ids 505 } 506 507 func gather_quote_ids(our_pubkey: Pubkey, from: NostrEvent) -> [RefId] { 508 var ids: [RefId] = [.quote(from.id.quote_id)] 509 if from.pubkey != our_pubkey { 510 ids.append(.pubkey(from.pubkey)) 511 } 512 return ids 513 } 514 515 func event_from_json(dat: String) -> NostrEvent? { 516 return NostrEvent.owned_from_json(json: dat) 517 } 518 519 func event_to_json(ev: NostrEvent) -> String { 520 let encoder = JSONEncoder() 521 guard let res = try? encoder.encode(ev) else { 522 return "{}" 523 } 524 guard let str = String(data: res, encoding: .utf8) else { 525 return "{}" 526 } 527 return str 528 } 529 530 func decrypt_dm(_ privkey: Privkey?, pubkey: Pubkey, content: String, encoding: EncEncoding) -> String? { 531 guard let privkey = privkey else { 532 return nil 533 } 534 guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: pubkey) else { 535 return nil 536 } 537 guard let dat = (encoding == .base64 ? decode_dm_base64(content) : decode_dm_bech32(content)) else { 538 return nil 539 } 540 guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else { 541 return nil 542 } 543 return String(data: dat, encoding: .utf8) 544 } 545 546 func decrypt_note(our_privkey: Privkey, their_pubkey: Pubkey, enc_note: String, encoding: EncEncoding) -> NostrEvent? { 547 guard let dec = decrypt_dm(our_privkey, pubkey: their_pubkey, content: enc_note, encoding: encoding) else { 548 return nil 549 } 550 551 return decode_nostr_event_json(json: dec) 552 } 553 554 func get_shared_secret(privkey: Privkey, pubkey: Pubkey) -> [UInt8]? { 555 let privkey_bytes = privkey.bytes 556 var pk_bytes = pubkey.bytes 557 558 pk_bytes.insert(2, at: 0) 559 560 var publicKey = secp256k1_pubkey() 561 var shared_secret = [UInt8](repeating: 0, count: 32) 562 563 var ok = 564 secp256k1_ec_pubkey_parse( 565 secp256k1.Context.raw, 566 &publicKey, 567 pk_bytes, 568 pk_bytes.count) != 0 569 570 if !ok { 571 return nil 572 } 573 574 ok = secp256k1_ecdh( 575 secp256k1.Context.raw, 576 &shared_secret, 577 &publicKey, 578 privkey_bytes, {(output,x32,_,_) in 579 memcpy(output,x32,32) 580 return 1 581 }, nil) != 0 582 583 if !ok { 584 return nil 585 } 586 587 return shared_secret 588 } 589 590 enum EncEncoding { 591 case base64 592 case bech32 593 } 594 595 struct DirectMessageBase64 { 596 let content: [UInt8] 597 let iv: [UInt8] 598 } 599 600 601 602 func encode_dm_bech32(content: [UInt8], iv: [UInt8]) -> String { 603 let content_bech32 = bech32_encode(hrp: "pzap", content) 604 let iv_bech32 = bech32_encode(hrp: "iv", iv) 605 return content_bech32 + "_" + iv_bech32 606 } 607 608 func decode_dm_bech32(_ all: String) -> DirectMessageBase64? { 609 let parts = all.split(separator: "_") 610 guard parts.count == 2 else { 611 return nil 612 } 613 614 let content_bech32 = String(parts[0]) 615 let iv_bech32 = String(parts[1]) 616 617 guard let content_tup = try? bech32_decode(content_bech32) else { 618 return nil 619 } 620 guard let iv_tup = try? bech32_decode(iv_bech32) else { 621 return nil 622 } 623 guard content_tup.hrp == "pzap" else { 624 return nil 625 } 626 guard iv_tup.hrp == "iv" else { 627 return nil 628 } 629 630 return DirectMessageBase64(content: content_tup.data.bytes, iv: iv_tup.data.bytes) 631 } 632 633 func encode_dm_base64(content: [UInt8], iv: [UInt8]) -> String { 634 let content_b64 = base64_encode(content) 635 let iv_b64 = base64_encode(iv) 636 return content_b64 + "?iv=" + iv_b64 637 } 638 639 func decode_dm_base64(_ all: String) -> DirectMessageBase64? { 640 let splits = Array(all.split(separator: "?")) 641 642 if splits.count != 2 { 643 return nil 644 } 645 646 guard let content = base64_decode(String(splits[0])) else { 647 return nil 648 } 649 650 var sec = String(splits[1]) 651 if !sec.hasPrefix("iv=") { 652 return nil 653 } 654 655 sec = String(sec.dropFirst(3)) 656 guard let iv = base64_decode(sec) else { 657 return nil 658 } 659 660 return DirectMessageBase64(content: content, iv: iv) 661 } 662 663 func base64_encode(_ content: [UInt8]) -> String { 664 return Data(content).base64EncodedString() 665 } 666 667 func base64_decode(_ content: String) -> [UInt8]? { 668 guard let dat = Data(base64Encoded: content) else { 669 return nil 670 } 671 return dat.bytes 672 } 673 674 func aes_decrypt(data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? { 675 return aes_operation(operation: CCOperation(kCCDecrypt), data: data, iv: iv, shared_sec: shared_sec) 676 } 677 678 func aes_encrypt(data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? { 679 return aes_operation(operation: CCOperation(kCCEncrypt), data: data, iv: iv, shared_sec: shared_sec) 680 } 681 682 func aes_operation(operation: CCOperation, data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? { 683 let data_len = data.count 684 let bsize = kCCBlockSizeAES128 685 let len = Int(data_len) + bsize 686 var decrypted_data = [UInt8](repeating: 0, count: len) 687 688 let key_length = size_t(kCCKeySizeAES256) 689 if shared_sec.count != key_length { 690 assert(false, "unexpected shared_sec len: \(shared_sec.count) != 32") 691 return nil 692 } 693 694 let algorithm: CCAlgorithm = UInt32(kCCAlgorithmAES128) 695 let options: CCOptions = UInt32(kCCOptionPKCS7Padding) 696 697 var num_bytes_decrypted :size_t = 0 698 699 let status = CCCrypt(operation, /*op:*/ 700 algorithm, /*alg:*/ 701 options, /*options:*/ 702 shared_sec, /*key:*/ 703 key_length, /*keyLength:*/ 704 iv, /*iv:*/ 705 data, /*dataIn:*/ 706 data_len, /*dataInLength:*/ 707 &decrypted_data,/*dataOut:*/ 708 len,/*dataOutAvailable:*/ 709 &num_bytes_decrypted/*dataOutMoved:*/ 710 ) 711 712 if UInt32(status) != UInt32(kCCSuccess) { 713 return nil 714 } 715 716 return Data(bytes: decrypted_data, count: num_bytes_decrypted) 717 718 } 719 720 721 722 func validate_event(ev: NostrEvent) -> ValidationResult { 723 let id = calculate_event_id(pubkey: ev.pubkey, created_at: ev.created_at, kind: ev.kind, tags: ev.tags.strings(), content: ev.content) 724 725 if id != ev.id { 726 return .bad_id 727 } 728 729 let ctx = secp256k1.Context.raw 730 var xonly_pubkey = secp256k1_xonly_pubkey.init() 731 732 var ev_pubkey = ev.pubkey.id.bytes 733 734 var ok = secp256k1_xonly_pubkey_parse(ctx, &xonly_pubkey, &ev_pubkey) != 0 735 if !ok { 736 return .bad_sig 737 } 738 739 var sig = ev.sig.data.bytes 740 var idbytes = id.id.bytes 741 742 ok = secp256k1_schnorrsig_verify(ctx, &sig, &idbytes, 32, &xonly_pubkey) > 0 743 return ok ? .ok : .bad_sig 744 } 745 746 func first_eref_mention(ev: NostrEvent, keypair: Keypair) -> Mention<NoteId>? { 747 let blocks = ev.blocks(keypair).blocks.filter { block in 748 guard case .mention(let mention) = block else { 749 return false 750 } 751 752 switch mention.ref { 753 case .note, .nevent: 754 return true 755 default: 756 return false 757 } 758 } 759 760 /// MARK: - Preview 761 if let firstBlock = blocks.first, 762 case .mention(let mention) = firstBlock { 763 switch mention.ref { 764 case .note(let note_id): 765 return .note(note_id) 766 case .nevent(let nevent): 767 return .note(nevent.noteid) 768 default: 769 return nil 770 } 771 } 772 return nil 773 } 774 775 func separate_invoices(ev: NostrEvent, keypair: Keypair) -> [Invoice]? { 776 let invoiceBlocks: [Invoice] = ev.blocks(keypair).blocks.reduce(into: []) { invoices, block in 777 guard case .invoice(let invoice) = block else { 778 return 779 } 780 invoices.append(invoice) 781 } 782 return invoiceBlocks.isEmpty ? nil : invoiceBlocks 783 } 784 785 /** 786 Transforms a `NostrEvent` of known kind `NostrKind.like`to a human-readable emoji. 787 If the known kind is not a `NostrKind.like`, it will return `nil`. 788 If the event content is an empty string or `+`, it will map that to a heart ❤️ emoji. 789 If the event content is a "-", it will map that to a dislike 👎 emoji. 790 Otherwise, it will return the event content at face value without transforming it. 791 */ 792 func to_reaction_emoji(ev: NostrEvent) -> String? { 793 guard ev.known_kind == NostrKind.like else { 794 return nil 795 } 796 797 switch ev.content { 798 case "", "+": 799 return "❤️" 800 case "-": 801 return "👎" 802 default: 803 return ev.content 804 } 805 } 806 807 extension NostrEvent { 808 /// The mutelist for a given event 809 /// 810 /// If the event is not a mutelist it will return `nil`. 811 var mute_list: Set<MuteItem>? { 812 if (self.kind == NostrKind.list_deprecated.rawValue && self.referenced_params.contains(where: { p in p.param.matches_str("mute") })) || self.kind == NostrKind.mute_list.rawValue { 813 return Set(self.referenced_mute_items) 814 } else { 815 return nil 816 } 817 } 818 }