Files
damus/damus/Nostr/NostrEvent.swift
2023-07-10 17:39:13 -07:00

1008 lines
28 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// NostrEvent.swift
// damus
//
// Created by William Casarin on 2022-04-11.
//
import Foundation
import CommonCrypto
import secp256k1
import secp256k1_implementation
import CryptoKit
import NaturalLanguage
enum ValidationResult: Decodable {
case unknown
case ok
case bad_id
case bad_sig
}
struct ReferencedId: Identifiable, Hashable, Equatable {
let ref_id: String
let relay_id: String?
let key: String
var id: String {
return ref_id
}
static func q(_ id: String, relay_id: String? = nil) -> ReferencedId {
return ReferencedId(ref_id: id, relay_id: relay_id, key: "q")
}
static func e(_ id: String, relay_id: String? = nil) -> ReferencedId {
return ReferencedId(ref_id: id, relay_id: relay_id, key: "e")
}
}
class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Hashable, Comparable {
static func == (lhs: NostrEvent, rhs: NostrEvent) -> Bool {
return lhs.id == rhs.id
}
static func < (lhs: NostrEvent, rhs: NostrEvent) -> Bool {
return lhs.created_at < rhs.created_at
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
var id: String
var sig: String
var tags: [[String]]
var boosted_by: String?
// cached field for pow calc
//var pow: Int?
// custom flags for internal use
var flags: Int = 0
let pubkey: String
let created_at: Int64
let kind: Int
let content: String
var is_textlike: Bool {
return kind == 1 || kind == 42
}
var too_big: Bool {
return self.content.utf8.count > 16000
}
var should_show_event: Bool {
return !too_big
}
var is_valid_id: Bool {
return calculate_event_id(ev: self) == self.id
}
private var _blocks: Blocks? = nil
func blocks(_ privkey: String?) -> Blocks {
if let bs = _blocks {
return bs
}
let blocks = get_blocks(content: self.get_content(privkey))
self._blocks = blocks
return blocks
}
func get_blocks(content: String) -> Blocks {
return parse_mentions(content: content, tags: self.tags)
}
private lazy var inner_event: NostrEvent? = {
return event_from_json(dat: self.content)
}()
func get_inner_event(cache: EventCache) -> NostrEvent? {
guard self.known_kind == .boost else {
return nil
}
if self.content == "", let ref = self.referenced_ids.first {
return cache.lookup(ref.ref_id)
}
return self.inner_event
}
private var _event_refs: [EventRef]? = nil
func event_refs(_ privkey: String?) -> [EventRef] {
if let rs = _event_refs {
return rs
}
let refs = interpret_event_refs(blocks: self.blocks(privkey).blocks, tags: self.tags)
self._event_refs = refs
return refs
}
var decrypted_content: String? = nil
func decrypted(privkey: String?) -> String? {
if let decrypted_content = decrypted_content {
return decrypted_content
}
guard let key = privkey else {
return nil
}
guard let our_pubkey = privkey_to_pubkey(privkey: key) else {
return nil
}
var pubkey = self.pubkey
// This is our DM, we need to use the pubkey of the person we're talking to instead
if our_pubkey == pubkey {
guard let refkey = self.referenced_pubkeys.first else {
return nil
}
pubkey = refkey.ref_id
}
let dec = decrypt_dm(key, pubkey: pubkey, content: self.content, encoding: .base64)
self.decrypted_content = dec
return dec
}
func get_content(_ privkey: String?) -> String {
if known_kind == .dm {
return decrypted(privkey: privkey) ?? "*failed to decrypt content*"
}
return content
}
var description: String {
return "NostrEvent { id: \(id) pubkey \(pubkey) kind \(kind) tags \(tags) content '\(content)' }"
}
var known_kind: NostrKind? {
return NostrKind.init(rawValue: kind)
}
private enum CodingKeys: String, CodingKey {
case id, sig, tags, pubkey, created_at, kind, content
}
private func get_referenced_ids(key: String) -> [ReferencedId] {
return damus.get_referenced_ids(tags: self.tags, key: key)
}
public func direct_replies(_ privkey: String?) -> [ReferencedId] {
return event_refs(privkey).reduce(into: []) { acc, evref in
if let direct_reply = evref.is_direct_reply {
acc.append(direct_reply)
}
}
}
public func thread_id(privkey: String?) -> String {
for ref in event_refs(privkey) {
if let thread_id = ref.is_thread_id {
return thread_id.ref_id
}
}
return self.id
}
public func last_refid() -> ReferencedId? {
var mlast: Int? = nil
var i: Int = 0
for tag in tags {
if tag.count >= 2 && tag[0] == "e" {
mlast = i
}
i += 1
}
guard let last = mlast else {
return nil
}
return tag_to_refid(tags[last])
}
public func references(id: String, key: String) -> Bool {
for tag in tags {
if tag.count >= 2 && tag[0] == key {
if tag[1] == id {
return true
}
}
}
return false
}
func is_reply(_ privkey: String?) -> Bool {
return event_is_reply(self, privkey: privkey)
}
func note_language(_ privkey: String?) -> String? {
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = blocks(privkey).blocks
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue else {
return nil
}
// Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
// Moreover, speakers of one variant can generally understand other variants.
return localeToLanguage(locale)
}
public var referenced_ids: [ReferencedId] {
return get_referenced_ids(key: "e")
}
public var referenced_pubkeys: [ReferencedId] {
return get_referenced_ids(key: "p")
}
public var is_local: Bool {
return (self.flags & 1) != 0
}
init(id: String = "", content: String, pubkey: String, kind: Int = 1, tags: [[String]] = [], createdAt: Int64 = Int64(Date().timeIntervalSince1970)) {
self.id = id
self.sig = ""
self.content = content
self.pubkey = pubkey
self.kind = kind
self.tags = tags
self.created_at = createdAt
}
func calculate_id() {
self.id = calculate_event_id(ev: self)
}
func sign(privkey: String) {
self.sig = sign_event(privkey: privkey, ev: self)
}
var age: TimeInterval {
let event_date = Date(timeIntervalSince1970: TimeInterval(created_at))
return Date.now.timeIntervalSince(event_date)
}
}
func sign_event(privkey: String, ev: NostrEvent) -> String {
let priv_key_bytes = try! privkey.bytes
let key = try! secp256k1.Signing.PrivateKey(rawRepresentation: priv_key_bytes)
// Extra params for custom signing
var aux_rand = random_bytes(count: 64)
var digest = try! ev.id.bytes
// API allows for signing variable length messages
let signature = try! key.schnorr.signature(message: &digest, auxiliaryRand: &aux_rand)
return hex_encode(signature.rawRepresentation)
}
func decode_nostr_event(txt: String) -> NostrResponse? {
return decode_data(Data(txt.utf8))
}
func encode_json<T: Encodable>(_ val: T) -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes
return (try? encoder.encode(val)).map { String(decoding: $0, as: UTF8.self) }
}
func decode_nostr_event_json(json: String) -> NostrEvent? {
return decode_json(json)
}
func decode_json<T: Decodable>(_ val: String) -> T? {
return try? JSONDecoder().decode(T.self, from: Data(val.utf8))
}
func decode_data<T: Decodable>(_ data: Data) -> T? {
let decoder = JSONDecoder()
do {
return try decoder.decode(T.self, from: data)
} catch {
print("decode_data failed for \(T.self): \(error)")
}
return nil
}
func event_commitment(ev: NostrEvent, tags: String) -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes
let str_data = try! encoder.encode(ev.content)
let content = String(decoding: str_data, as: UTF8.self)
let commit = "[0,\"\(ev.pubkey)\",\(ev.created_at),\(ev.kind),\(tags),\(content)]"
//print("COMMIT", commit)
return commit
}
func calculate_event_commitment(ev: NostrEvent) -> Data {
let tags_encoder = JSONEncoder()
tags_encoder.outputFormatting = .withoutEscapingSlashes
let tags_data = try! tags_encoder.encode(ev.tags)
let tags = String(decoding: tags_data, as: UTF8.self)
let target = event_commitment(ev: ev, tags: tags)
let target_data = target.data(using: .utf8)!
return target_data
}
func calculate_event_id(ev: NostrEvent) -> String {
let commitment = calculate_event_commitment(ev: ev)
let hash = sha256(commitment)
return hex_encode(hash)
}
func sha256(_ data: Data) -> Data {
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
}
return Data(hash)
}
func hexchar(_ val: UInt8) -> UInt8 {
if val < 10 {
return 48 + val;
}
if val < 16 {
return 97 + val - 10;
}
assertionFailure("impossiburu")
return 0
}
func hex_encode(_ data: Data) -> String {
var str = ""
for c in data {
let c1 = hexchar(c >> 4)
let c2 = hexchar(c & 0xF)
str.append(Character(Unicode.Scalar(c1)))
str.append(Character(Unicode.Scalar(c2)))
}
return str
}
func random_bytes(count: Int) -> Data {
var bytes = [Int8](repeating: 0, count: count)
guard
SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) == errSecSuccess
else {
fatalError("can't copy secure random data")
}
return Data(bytes: bytes, count: count)
}
func refid_to_tag(_ ref: ReferencedId) -> [String] {
var tag = [ref.key, ref.ref_id]
if let relay_id = ref.relay_id {
tag.append(relay_id)
}
return tag
}
func tag_to_refid(_ tag: [String]) -> ReferencedId? {
if tag.count == 0 {
return nil
}
if tag.count == 1 {
return nil
}
var relay_id: String? = nil
if tag.count > 2 {
relay_id = tag[2]
}
return ReferencedId(ref_id: tag[1], relay_id: relay_id, key: tag[0])
}
func get_referenced_ids(tags: [[String]], key: String) -> [ReferencedId] {
return tags.reduce(into: []) { (acc, tag) in
if tag.count >= 2 && tag[0] == key {
var relay_id: String? = nil
if tag.count >= 3 {
relay_id = tag[2]
}
acc.append(ReferencedId(ref_id: tag[1], relay_id: relay_id, key: key))
}
}
}
func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
guard let privkey = keypair.privkey else {
return nil
}
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
let rw_relay_info = RelayInfo(read: true, write: true)
var relays: [String: RelayInfo] = [:]
for relay in bootstrap_relays {
relays[relay] = rw_relay_info
}
let relay_json = encode_json(relays)!
let damus_pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let jb55_pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" // lol
let tags = [
["p", damus_pubkey],
["p", jb55_pubkey],
["p", keypair.pubkey] // you're a friend of yourself!
]
let ev = NostrEvent(content: relay_json,
pubkey: keypair.pubkey,
kind: NostrKind.contacts.rawValue,
tags: tags)
ev.calculate_id()
ev.sign(privkey: privkey)
return ev
}
func make_metadata_event(keypair: FullKeypair, metadata: Profile) -> NostrEvent {
let metadata_json = encode_json(metadata)!
let ev = NostrEvent(content: metadata_json,
pubkey: keypair.pubkey,
kind: NostrKind.metadata.rawValue,
tags: [])
ev.calculate_id()
ev.sign(privkey: keypair.privkey)
return ev
}
func make_boost_event(pubkey: String, privkey: String, boosted: NostrEvent) -> NostrEvent {
var tags: [[String]] = boosted.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
tags.append(["e", boosted.id, "", "root"])
tags.append(["p", boosted.pubkey])
let ev = NostrEvent(content: event_to_json(ev: boosted), pubkey: pubkey, kind: 6, tags: tags)
ev.calculate_id()
ev.sign(privkey: privkey)
return ev
}
func make_like_event(pubkey: String, privkey: String, liked: NostrEvent, content: String = "🤙") -> NostrEvent {
var tags: [[String]] = liked.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
tags.append(["e", liked.id])
tags.append(["p", liked.pubkey])
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 7, tags: tags)
ev.calculate_id()
ev.sign(privkey: privkey)
return ev
}
func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
switch target {
case .profile(let pk):
return [["p", pk]]
case .note(let note_target):
return [["e", note_target.note_id], ["p", note_target.author]]
}
}
struct PrivateZapRequest {
let req: ZapRequest
let enc: String
}
func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair, target: ZapTarget, message: String) -> PrivateZapRequest? {
// target tags must be the same as zap request target tags
let tags = zap_target_to_tags(target)
let note = NostrEvent(content: message, pubkey: identity.pubkey, kind: 9733, tags: tags)
note.id = calculate_event_id(ev: note)
note.sig = sign_event(privkey: identity.privkey, ev: note)
guard let note_json = encode_json(note),
let enc = encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
else {
return nil
}
return PrivateZapRequest(req: ZapRequest(ev: note), enc: enc)
}
func decrypt_private_zap(our_privkey: String, zapreq: NostrEvent, target: ZapTarget) -> NostrEvent? {
guard let anon_tag = zapreq.tags.first(where: { t in t.count >= 2 && t[0] == "anon" }) else {
return nil
}
let enc_note = anon_tag[1]
var note = decrypt_note(our_privkey: our_privkey, their_pubkey: zapreq.pubkey, enc_note: enc_note, encoding: .bech32)
// check to see if the private note was from us
if note == nil {
guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: target.id, created_at: zapreq.created_at) else{
return nil
}
// use our private keypair and their pubkey to get the shared secret
note = decrypt_note(our_privkey: our_private_keypair.privkey, their_pubkey: target.pubkey, enc_note: enc_note, encoding: .bech32)
}
guard let note else {
return nil
}
guard note.kind == 9733 else {
return nil
}
let zr_etag = zapreq.referenced_ids.first
let note_etag = note.referenced_ids.first
guard zr_etag == note_etag else {
return nil
}
let zr_ptag = zapreq.referenced_pubkeys.first
let note_ptag = note.referenced_pubkeys.first
guard let zr_ptag, let note_ptag, zr_ptag == note_ptag else {
return nil
}
guard validate_event(ev: note) == .ok else {
return nil
}
return note
}
func generate_private_keypair(our_privkey: String, id: String, created_at: Int64) -> FullKeypair? {
let to_hash = our_privkey + id + String(created_at)
guard let dat = to_hash.data(using: .utf8) else {
return nil
}
let privkey_bytes = sha256(dat)
let privkey = hex_encode(privkey_bytes)
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
return nil
}
return FullKeypair(pubkey: pubkey, privkey: privkey)
}
enum MakeZapRequest {
case priv(ZapRequest, PrivateZapRequest)
case normal(ZapRequest)
var private_inner_request: ZapRequest {
switch self {
case .priv(_, let pzr):
return pzr.req
case .normal(let zr):
return zr
}
}
var potentially_anon_outer_request: ZapRequest {
switch self {
case .priv(let zr, _):
return zr
case .normal(let zr):
return zr
}
}
}
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> MakeZapRequest? {
var tags = zap_target_to_tags(target)
var relay_tag = ["relays"]
relay_tag.append(contentsOf: relays.map { $0.url.id })
tags.append(relay_tag)
var kp = keypair
let now = Int64(Date().timeIntervalSince1970)
var privzap_req: PrivateZapRequest?
var message = content
switch zap_type {
case .pub:
break
case .non_zap:
break
case .anon:
tags.append(["anon"])
kp = generate_new_keypair().to_full()!
case .priv:
guard let priv_kp = generate_private_keypair(our_privkey: keypair.privkey, id: target.id, created_at: now) else {
return nil
}
kp = priv_kp
guard let privreq = make_private_zap_request_event(identity: keypair, enc_key: kp, target: target, message: message) else {
return nil
}
tags.append(["anon", privreq.enc])
message = ""
privzap_req = privreq
}
let ev = NostrEvent(content: message, pubkey: kp.pubkey, kind: 9734, tags: tags, createdAt: now)
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: kp.privkey, ev: ev)
let zapreq = ZapRequest(ev: ev)
if let privzap_req {
return .priv(zapreq, privzap_req)
} else {
return .normal(zapreq)
}
}
func uniq<T: Hashable>(_ xs: [T]) -> [T] {
var s = Set<T>()
var ys: [T] = []
for x in xs {
if s.contains(x) {
continue
}
s.insert(x)
ys.append(x)
}
return ys
}
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
ids.append(.e(from.id))
ids.append(contentsOf: uniq(from.referenced_pubkeys.filter { $0.ref_id != our_pubkey }))
if from.pubkey != our_pubkey {
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
}
return ids
}
func gather_quote_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids: [ReferencedId] = [.q(from.id)]
if from.pubkey != our_pubkey {
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
}
return ids
}
func event_from_json(dat: String) -> NostrEvent? {
return try? JSONDecoder().decode(NostrEvent.self, from: Data(dat.utf8))
}
func event_to_json(ev: NostrEvent) -> String {
let encoder = JSONEncoder()
guard let res = try? encoder.encode(ev) else {
return "{}"
}
guard let str = String(data: res, encoding: .utf8) else {
return "{}"
}
return str
}
func decrypt_dm(_ privkey: String?, pubkey: String, content: String, encoding: EncEncoding) -> String? {
guard let privkey = privkey else {
return nil
}
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: pubkey) else {
return nil
}
guard let dat = (encoding == .base64 ? decode_dm_base64(content) : decode_dm_bech32(content)) else {
return nil
}
guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else {
return nil
}
return String(data: dat, encoding: .utf8)
}
func decrypt_note(our_privkey: String, their_pubkey: String, enc_note: String, encoding: EncEncoding) -> NostrEvent? {
guard let dec = decrypt_dm(our_privkey, pubkey: their_pubkey, content: enc_note, encoding: encoding) else {
return nil
}
return decode_nostr_event_json(json: dec)
}
func get_shared_secret(privkey: String, pubkey: String) -> [UInt8]? {
guard let privkey_bytes = try? privkey.bytes else {
return nil
}
guard var pk_bytes = try? pubkey.bytes else {
return nil
}
pk_bytes.insert(2, at: 0)
var publicKey = secp256k1_pubkey()
var shared_secret = [UInt8](repeating: 0, count: 32)
var ok =
secp256k1_ec_pubkey_parse(
secp256k1.Context.raw,
&publicKey,
pk_bytes,
pk_bytes.count) != 0
if !ok {
return nil
}
ok = secp256k1_ecdh(
secp256k1.Context.raw,
&shared_secret,
&publicKey,
privkey_bytes, {(output,x32,_,_) in
memcpy(output,x32,32)
return 1
}, nil) != 0
if !ok {
return nil
}
return shared_secret
}
struct DirectMessageBase64 {
let content: [UInt8]
let iv: [UInt8]
}
func encode_dm_bech32(content: [UInt8], iv: [UInt8]) -> String {
let content_bech32 = bech32_encode(hrp: "pzap", content)
let iv_bech32 = bech32_encode(hrp: "iv", iv)
return content_bech32 + "_" + iv_bech32
}
func decode_dm_bech32(_ all: String) -> DirectMessageBase64? {
let parts = all.split(separator: "_")
guard parts.count == 2 else {
return nil
}
let content_bech32 = String(parts[0])
let iv_bech32 = String(parts[1])
guard let content_tup = try? bech32_decode(content_bech32) else {
return nil
}
guard let iv_tup = try? bech32_decode(iv_bech32) else {
return nil
}
guard content_tup.hrp == "pzap" else {
return nil
}
guard iv_tup.hrp == "iv" else {
return nil
}
return DirectMessageBase64(content: content_tup.data.bytes, iv: iv_tup.data.bytes)
}
func encode_dm_base64(content: [UInt8], iv: [UInt8]) -> String {
let content_b64 = base64_encode(content)
let iv_b64 = base64_encode(iv)
return content_b64 + "?iv=" + iv_b64
}
func decode_dm_base64(_ all: String) -> DirectMessageBase64? {
let splits = Array(all.split(separator: "?"))
if splits.count != 2 {
return nil
}
guard let content = base64_decode(String(splits[0])) else {
return nil
}
var sec = String(splits[1])
if !sec.hasPrefix("iv=") {
return nil
}
sec = String(sec.dropFirst(3))
guard let iv = base64_decode(sec) else {
return nil
}
return DirectMessageBase64(content: content, iv: iv)
}
func base64_encode(_ content: [UInt8]) -> String {
return Data(content).base64EncodedString()
}
func base64_decode(_ content: String) -> [UInt8]? {
guard let dat = Data(base64Encoded: content) else {
return nil
}
return dat.bytes
}
func aes_decrypt(data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? {
return aes_operation(operation: CCOperation(kCCDecrypt), data: data, iv: iv, shared_sec: shared_sec)
}
func aes_encrypt(data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? {
return aes_operation(operation: CCOperation(kCCEncrypt), data: data, iv: iv, shared_sec: shared_sec)
}
func aes_operation(operation: CCOperation, data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? {
let data_len = data.count
let bsize = kCCBlockSizeAES128
let len = Int(data_len) + bsize
var decrypted_data = [UInt8](repeating: 0, count: len)
let key_length = size_t(kCCKeySizeAES256)
if shared_sec.count != key_length {
assert(false, "unexpected shared_sec len: \(shared_sec.count) != 32")
return nil
}
let algorithm: CCAlgorithm = UInt32(kCCAlgorithmAES128)
let options: CCOptions = UInt32(kCCOptionPKCS7Padding)
var num_bytes_decrypted :size_t = 0
let status = CCCrypt(operation, /*op:*/
algorithm, /*alg:*/
options, /*options:*/
shared_sec, /*key:*/
key_length, /*keyLength:*/
iv, /*iv:*/
data, /*dataIn:*/
data_len, /*dataInLength:*/
&decrypted_data,/*dataOut:*/
len,/*dataOutAvailable:*/
&num_bytes_decrypted/*dataOutMoved:*/
)
if UInt32(status) != UInt32(kCCSuccess) {
return nil
}
return Data(bytes: decrypted_data, count: num_bytes_decrypted)
}
func validate_event(ev: NostrEvent) -> ValidationResult {
let raw_id = sha256(calculate_event_commitment(ev: ev))
let id = hex_encode(raw_id)
if id != ev.id {
return .bad_id
}
// TODO: implement verify
guard var sig64 = hex_decode(ev.sig)?.bytes else {
return .bad_sig
}
guard var ev_pubkey = hex_decode(ev.pubkey)?.bytes else {
return .bad_sig
}
let ctx = secp256k1.Context.raw
var xonly_pubkey = secp256k1_xonly_pubkey.init()
var ok = secp256k1_xonly_pubkey_parse(ctx, &xonly_pubkey, &ev_pubkey) != 0
if !ok {
return .bad_sig
}
var raw_id_bytes = raw_id.bytes
ok = secp256k1_schnorrsig_verify(ctx, &sig64, &raw_id_bytes, raw_id.count, &xonly_pubkey) > 0
return ok ? .ok : .bad_sig
}
func last_etag(tags: [[String]]) -> String? {
var e: String? = nil
for tag in tags {
if tag.count >= 2 && tag[0] == "e" {
e = tag[1]
}
}
return e
}
func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
let blocks = ev.blocks(privkey).blocks.filter { block in
guard case .mention(let mention) = block else {
return false
}
guard case .event = mention.type else {
return false
}
if mention.ref.key != "e" {
return false
}
return true
}
/// MARK: - Preview
if let firstBlock = blocks.first, case .mention(let mention) = firstBlock, mention.ref.key == "e" {
return mention
}
return nil
}
/**
Transforms a `NostrEvent` of known kind `NostrKind.like`to a human-readable emoji.
If the known kind is not a `NostrKind.like`, it will return `nil`.
If the event content is an empty string or `+`, it will map that to a heart emoji.
If the event content is a "-", it will map that to a dislike 👎 emoji.
Otherwise, it will return the event content at face value without transforming it.
*/
func to_reaction_emoji(ev: NostrEvent) -> String? {
guard ev.known_kind == NostrKind.like else {
return nil
}
switch ev.content {
case "", "+":
return "❤️"
case "-":
return "👎"
default:
return ev.content
}
}
extension [ReferencedId] {
var pRefs: [ReferencedId] {
get {
Set(self).filter { ref in
ref.key == "p"
}
}
}
var eRefs: [ReferencedId] {
get {
self.filter { ref in
ref.key == "e"
}
}
}
}