Private Zaps

This adds private zaps, which have messages and authors encrypted to
the target. Keys are deterministically generated so that both the
receiver and sender can decrypt.

Changelog-Added: Private Zaps
This commit is contained in:
William Casarin
2023-03-01 07:43:44 -08:00
parent c72c0079cc
commit 77f5268336
18 changed files with 359 additions and 92 deletions

View File

@@ -138,7 +138,7 @@ struct ZapButton_Previews: PreviewProvider {
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) { func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
guard let privkey = damus_state.keypair.privkey else { guard let keypair = damus_state.keypair.to_full() else {
return return
} }
@@ -146,7 +146,8 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
let relays = Array(damus_state.pool.descriptors.prefix(10)) let relays = Array(damus_state.pool.descriptors.prefix(10))
let target = ZapTarget.note(id: event.id, author: event.pubkey) let target = ZapTarget.note(id: event.id, author: event.pubkey)
let content = comment ?? "" let content = comment ?? ""
let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target, is_anon: zap_type == .anon)
let zapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type)
Task { Task {
var mpayreq = damus_state.lnurls.lookup(target.pubkey) var mpayreq = damus_state.lnurls.lookup(target.pubkey)

View File

@@ -130,14 +130,14 @@ class HomeModel: ObservableObject {
} }
} }
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_pubkey: String, zapper: String) { func handle_zap_event_with_zapper(_ ev: NostrEvent, our_keypair: Keypair, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else { guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return return
} }
damus_state.zaps.add_zap(zap: zap) damus_state.zaps.add_zap(zap: zap)
guard zap.target.pubkey == our_pubkey else { guard zap.target.pubkey == our_keypair.pubkey else {
return return
} }
@@ -155,8 +155,9 @@ class HomeModel: ObservableObject {
return return
} }
let our_keypair = damus_state.keypair
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) { if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
handle_zap_event_with_zapper(ev, our_pubkey: damus_state.pubkey, zapper: local_zapper) handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: local_zapper)
return return
} }
@@ -175,7 +176,7 @@ class HomeModel: ObservableObject {
DispatchQueue.main.async { DispatchQueue.main.async {
self.damus_state.profiles.zappers[ptag] = zapper self.damus_state.profiles.zappers[ptag] = zapper
self.handle_zap_event_with_zapper(ev, our_pubkey: self.damus_state.pubkey, zapper: zapper) self.handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: zapper)
} }
} }

View File

@@ -21,7 +21,13 @@ class ZapGroup {
} }
func zap_requests() -> [NostrEvent] { func zap_requests() -> [NostrEvent] {
zaps.map { z in z.request.ev } zaps.map { z in
if let priv = z.private_request {
return priv
} else {
return z.request.ev
}
}
} }
init(zaps: [Zap]) { init(zaps: [Zap]) {

View File

@@ -65,7 +65,7 @@ class ZapsModel: ObservableObject {
return return
} }
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else { guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else {
return return
} }

View File

@@ -157,7 +157,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
pubkey = refkey.ref_id pubkey = refkey.ref_id
} }
let dec = decrypt_dm(key, pubkey: pubkey, content: self.content) let dec = decrypt_dm(key, pubkey: pubkey, content: self.content, encoding: .base64)
self.decrypted_content = dec self.decrypted_content = dec
return dec return dec
@@ -577,25 +577,115 @@ func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
} }
} }
func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget, is_anon: Bool) -> NostrEvent { func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair, target: ZapTarget, message: String) -> String? {
// 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) else {
return nil
}
return encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
}
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)
}
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> NostrEvent? {
var tags = zap_target_to_tags(target) var tags = zap_target_to_tags(target)
var relay_tag = ["relays"] var relay_tag = ["relays"]
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString }) relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
tags.append(relay_tag) tags.append(relay_tag)
var priv = privkey var kp = keypair
var pub = pubkey
if is_anon { let now = Int64(Date().timeIntervalSince1970)
var message = content
switch zap_type {
case .pub:
break
case .non_zap:
break
case .anon:
tags.append(["anon"]) tags.append(["anon"])
let kp = generate_new_keypair() kp = generate_new_keypair().to_full()!
pub = kp.pubkey case .priv:
priv = kp.privkey! 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])
message = ""
} }
let ev = NostrEvent(content: content, pubkey: pub, kind: 9734, tags: tags) let ev = NostrEvent(content: message, pubkey: kp.pubkey, kind: 9734, tags: tags, createdAt: now)
ev.id = calculate_event_id(ev: ev) ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: priv, ev: ev) ev.sig = sign_event(privkey: kp.privkey, ev: ev)
return ev return ev
} }
@@ -625,14 +715,14 @@ func event_to_json(ev: NostrEvent) -> String {
return str return str
} }
func decrypt_dm(_ privkey: String?, pubkey: String, content: String) -> String? { func decrypt_dm(_ privkey: String?, pubkey: String, content: String, encoding: EncEncoding) -> String? {
guard let privkey = privkey else { guard let privkey = privkey else {
return nil return nil
} }
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: pubkey) else { guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: pubkey) else {
return nil return nil
} }
guard let dat = decode_dm_base64(content) else { guard let dat = (encoding == .base64 ? decode_dm_base64(content) : decode_dm_bech32(content)) else {
return nil return nil
} }
guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else { guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else {
@@ -641,6 +731,13 @@ func decrypt_dm(_ privkey: String?, pubkey: String, content: String) -> String?
return String(data: dat, encoding: .utf8) 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]? { func get_shared_secret(privkey: String, pubkey: String) -> [UInt8]? {
guard let privkey_bytes = try? privkey.bytes else { guard let privkey_bytes = try? privkey.bytes else {
@@ -686,6 +783,39 @@ struct DirectMessageBase64 {
let iv: [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 { func encode_dm_base64(content: [UInt8], iv: [UInt8]) -> String {
let content_b64 = base64_encode(content) let content_b64 = base64_encode(content)
let iv_b64 = base64_encode(iv) let iv_b64 = base64_encode(iv)

View File

@@ -7,12 +7,6 @@
import Foundation import Foundation
enum ZapSource {
case author(String)
// TODO: anonymous
//case anonymous
}
public struct NoteZapTarget: Equatable { public struct NoteZapTarget: Equatable {
public let note_id: String public let note_id: String
public let author: String public let author: String
@@ -55,8 +49,10 @@ struct Zap {
public let zapper: String /// zap authorizer public let zapper: String /// zap authorizer
public let target: ZapTarget public let target: ZapTarget
public let request: ZapRequest public let request: ZapRequest
public let is_anon: Bool
public let private_request: NostrEvent?
public static func from_zap_event(zap_ev: NostrEvent, zapper: String) -> Zap? { public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? {
/// Make sure that we only create a zap event if it is authorized by the profile or event /// Make sure that we only create a zap event if it is authorized by the profile or event
guard zapper == zap_ev.pubkey else { guard zapper == zap_ev.pubkey else {
return nil return nil
@@ -83,14 +79,26 @@ struct Zap {
guard let desc = get_zap_description(zap_ev, inv_desc: zap_invoice.description) else { guard let desc = get_zap_description(zap_ev, inv_desc: zap_invoice.description) else {
return nil return nil
} }
guard let zap_req = decode_nostr_event_json(desc) else { guard let zap_req = decode_nostr_event_json(desc) else {
return nil return nil
} }
guard validate_event(ev: zap_req) == .ok else {
return nil
}
guard let target = determine_zap_target(zap_req) else { guard let target = determine_zap_target(zap_req) else {
return nil return nil
} }
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req)) let private_request = our_privkey.flatMap {
decrypt_private_zap(our_privkey: $0, zapreq: zap_req, target: target)
}
let is_anon = private_request == nil && event_is_anonymous(ev: zap_req)
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: private_request)
} }
} }
@@ -285,7 +293,7 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
return endpoint return endpoint
} }
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int, zap_type: ZapType, comment: String?) async -> String? { func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, sats: Int, zap_type: ZapType, comment: String?) async -> String? {
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else { guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
return nil return nil
} }
@@ -295,11 +303,10 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int,
var query = [URLQueryItem(name: "amount", value: "\(amount)")] var query = [URLQueryItem(name: "amount", value: "\(amount)")]
if zappable && zap_type != .non_zap { if let zapreq, zappable && zap_type != .non_zap {
if let json = encode_json(zapreq) { let json = event_to_json(ev: zapreq)
print("zapreq json: \(json)") print("zapreq json: \(json)")
query.append(URLQueryItem(name: "nostr", value: json)) query.append(URLQueryItem(name: "nostr", value: json))
}
} }
// add a lud12 comment as well if we have it // add a lud12 comment as well if we have it

View File

@@ -181,13 +181,12 @@ struct DMChatView_Previews: PreviewProvider {
} }
} }
enum EncEncoding {
case base64
case bech32
}
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent? func encrypt_message(message: String, privkey: String, to_pk: String, encoding: EncEncoding = .base64) -> String? {
{
guard let privkey = keypair.privkey else {
return nil
}
let iv = random_bytes(count: 16).bytes let iv = random_bytes(count: 16).bytes
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else { guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
return nil return nil
@@ -196,7 +195,26 @@ func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keyp
guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else { guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
return nil return nil
} }
let enc_content = encode_dm_base64(content: enc_message.bytes, iv: iv)
switch encoding {
case .base64:
return encode_dm_base64(content: enc_message.bytes, iv: iv)
case .bech32:
return encode_dm_bech32(content: enc_message.bytes, iv: iv)
}
}
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent?
{
guard let privkey = keypair.privkey else {
return nil
}
guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else {
return nil
}
let created = created_at ?? Int64(Date().timeIntervalSince1970) let created = created_at ?? Int64(Date().timeIntervalSince1970)
let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: 4, tags: tags, createdAt: created) let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: 4, tags: tags, createdAt: created)

View File

@@ -29,29 +29,29 @@ func eventviewsize_to_font(_ size: EventViewKind) -> Font {
struct EventView: View { struct EventView: View {
let event: NostrEvent let event: NostrEvent
let has_action_bar: Bool let options: EventViewOptions
let damus: DamusState let damus: DamusState
let pubkey: String let pubkey: String
@EnvironmentObject var action_bar: ActionBarModel @EnvironmentObject var action_bar: ActionBarModel
init(damus: DamusState, event: NostrEvent, has_action_bar: Bool) { init(damus: DamusState, event: NostrEvent, options: EventViewOptions) {
self.event = event self.event = event
self.has_action_bar = has_action_bar self.options = options
self.damus = damus self.damus = damus
self.pubkey = event.pubkey self.pubkey = event.pubkey
} }
init(damus: DamusState, event: NostrEvent) { init(damus: DamusState, event: NostrEvent) {
self.event = event self.event = event
self.has_action_bar = false self.options = []
self.damus = damus self.damus = damus
self.pubkey = event.pubkey self.pubkey = event.pubkey
} }
init(damus: DamusState, event: NostrEvent, pubkey: String) { init(damus: DamusState, event: NostrEvent, pubkey: String) {
self.event = event self.event = event
self.has_action_bar = false self.options = [.no_action_bar]
self.damus = damus self.damus = damus
self.pubkey = pubkey self.pubkey = pubkey
} }
@@ -68,7 +68,7 @@ struct EventView: View {
Reposted(damus: damus, pubkey: event.pubkey, profile: prof) Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
TextEvent(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, has_action_bar: has_action_bar, booster_pubkey: event.pubkey) TextEvent(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, options: options)
.padding([.top], 1) .padding([.top], 1)
} }
} else { } else {
@@ -81,7 +81,7 @@ struct EventView: View {
EmptyView() EmptyView()
} }
} else { } else {
TextEvent(damus: damus, event: event, pubkey: pubkey, has_action_bar: has_action_bar, booster_pubkey: nil) TextEvent(damus: damus, event: event, pubkey: pubkey, options: options)
.padding([.top], 6) .padding([.top], 6)
} }
} }
@@ -176,11 +176,7 @@ struct EventView_Previews: PreviewProvider {
EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .big) EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .big)
*/ */
EventView( EventView( damus: test_damus_state(), event: test_event )
damus: test_damus_state(),
event: test_event,
has_action_bar: true
)
} }
.padding() .padding()
} }

View File

@@ -57,7 +57,7 @@ struct MutedEventView: View {
if selected { if selected {
SelectedEventView(damus: damus_state, event: event) SelectedEventView(damus: damus_state, event: event)
} else { } else {
EventView(damus: damus_state, event: event, has_action_bar: true) EventView(damus: damus_state, event: event)
.onTapGesture { .onTapGesture {
nav_target = event.id nav_target = event.id
navigating = true navigating = true

View File

@@ -7,12 +7,22 @@
import SwiftUI import SwiftUI
struct EventViewOptions: OptionSet {
let rawValue: UInt8
static let no_action_bar = EventViewOptions(rawValue: 1 << 0)
static let no_replying_to = EventViewOptions(rawValue: 1 << 1)
static let no_images = EventViewOptions(rawValue: 1 << 2)
}
struct TextEvent: View { struct TextEvent: View {
let damus: DamusState let damus: DamusState
let event: NostrEvent let event: NostrEvent
let pubkey: String let pubkey: String
let has_action_bar: Bool let options: EventViewOptions
let booster_pubkey: String?
var has_action_bar: Bool {
!options.contains(.no_action_bar)
}
var body: some View { var body: some View {
HStack(alignment: .top) { HStack(alignment: .top) {
@@ -62,7 +72,7 @@ struct TextEvent: View {
struct TextEvent_Previews: PreviewProvider { struct TextEvent_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", has_action_bar: true, booster_pubkey: nil) TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", options: [])
} }
} }

View File

@@ -13,21 +13,44 @@ struct ZapEvent: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("⚡️ \(format_msats(zap.invoice.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user") HStack(alignment: .center) {
.font(.headline) Text("⚡️ \(format_msats(zap.invoice.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
.padding([.top], 2) .font(.headline)
.padding([.top], 2)
if zap.private_request != nil {
Image(systemName: "lock.fill")
.foregroundColor(Color("DamusGreen"))
.help("Only you can see this message and who sent it.")
}
}
TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, has_action_bar: false, booster_pubkey: nil) if let priv = zap.private_request {
.padding([.top], 1)
TextEvent(damus: damus, event: priv, pubkey: priv.pubkey, options: [.no_action_bar, .no_replying_to])
.padding([.top], 1)
} else {
TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, options: [.no_action_bar, .no_replying_to])
.padding([.top], 1)
}
} }
} }
} }
/*
let test_zap_invoice = ZapInvoice(description: .description("description"), amount: 10000, string: "lnbc1", expiry: 1000000, payment_hash: Data(), created_at: 1000000)
let test_zap_request_ev = NostrEvent(content: "hi", pubkey: "pk", kind: 9734)
let test_zap_request = ZapRequest(ev: test_zap_request_ev)
let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: nil)
let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: test_event)
struct ZapEvent_Previews: PreviewProvider { struct ZapEvent_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ZapEvent() VStack {
ZapEvent(damus: test_damus_state(), zap: test_zap)
ZapEvent(damus: test_damus_state(), zap: test_private_zap)
}
} }
} }
*/

View File

@@ -14,6 +14,19 @@ enum EventGroupType {
case zap(ZapGroup) case zap(ZapGroup)
case profile_zap(ZapGroup) case profile_zap(ZapGroup)
var zap_group: ZapGroup? {
switch self {
case .profile_zap(let grp):
return grp
case .zap(let grp):
return grp
case .reaction:
return nil
case .repost:
return nil
}
}
var events: [NostrEvent] { var events: [NostrEvent] {
switch self { switch self {
case .repost(let grp): case .repost(let grp):
@@ -46,10 +59,28 @@ func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo {
return .tagged_in return .tagged_in
} }
func event_author_name(profiles: Profiles, _ ev: NostrEvent) -> String { func event_author_name(profiles: Profiles, pubkey: String) -> String {
let alice_pk = ev.pubkey let alice_prof = profiles.lookup(id: pubkey)
let alice_prof = profiles.lookup(id: alice_pk) return Profile.displayName(profile: alice_prof, pubkey: pubkey)
return Profile.displayName(profile: alice_prof, pubkey: alice_pk) }
func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType) -> String {
if let zapgrp = group.zap_group {
let zap = zapgrp.zaps[ind]
if let privzap = zap.private_request {
return event_author_name(profiles: profiles, pubkey: privzap.pubkey)
}
if zap.is_anon {
return "Anonymous"
}
return event_author_name(profiles: profiles, pubkey: zap.request.ev.pubkey)
} else {
let ev = group.events[ind]
return event_author_name(profiles: profiles, pubkey: ev.pubkey)
}
} }
/** /**
@@ -99,18 +130,16 @@ func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupT
case 0: case 0:
return NSLocalizedString("??", comment: "") return NSLocalizedString("??", comment: "")
case 1: case 1:
let ev = group.events.first! let display_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
let profile = profiles.lookup(id: ev.pubkey)
let display_name = Profile.displayName(profile: profile, pubkey: ev.pubkey)
return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, display_name) return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, display_name)
case 2: case 2:
let alice_name = event_author_name(profiles: profiles, group.events[0]) let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
let bob_name = event_author_name(profiles: profiles, group.events[1]) let bob_name = event_group_author_name(profiles: profiles, ind: 1, group: group)
return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, alice_name, bob_name) return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, alice_name, bob_name)
default: default:
let alice_name = event_author_name(profiles: profiles, group.events.first!) let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
let count = group.events.count - 1 let count = group.events.count - 1
return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, count, alice_name) return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, count, alice_name)

View File

@@ -52,7 +52,7 @@ struct NotificationItemView: View {
case .reply(let ev): case .reply(let ev):
NavigationLink(destination: BuildThreadV2View(damus: state, event_id: ev.id)) { NavigationLink(destination: BuildThreadV2View(damus: state, event_id: ev.id)) {
EventView(damus: state, event: ev, has_action_bar: true) EventView(damus: state, event: ev)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }

View File

@@ -45,7 +45,7 @@ struct ReplyView: View {
ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences) ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences)
} }
ScrollView { ScrollView {
EventView(damus: damus, event: replying_to, has_action_bar: false) EventView(damus: damus, event: replying_to, options: [.no_action_bar])
} }
PostView(replying_to: replying_to, references: references, damus_state: damus) PostView(replying_to: replying_to, references: references, damus_state: damus)
} }

View File

@@ -36,7 +36,7 @@ struct InnerTimelineView: View {
EmptyTimelineView() EmptyTimelineView()
} else { } else {
ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in
EventView(damus: damus, event: ev, has_action_bar: true) EventView(damus: damus, event: ev)
.onTapGesture { .onTapGesture {
nav_target = ev nav_target = ev
navigating = true navigating = true

View File

@@ -11,6 +11,7 @@ import Combine
enum ZapType { enum ZapType {
case pub case pub
case anon case anon
case priv
case non_zap case non_zap
} }
@@ -80,11 +81,29 @@ struct CustomizeZapView: View {
self.state = state self.state = state
} }
var zap_type_desc: String {
switch zap_type {
case .pub:
return "Everyone on can see that you zapped"
case .anon:
return "Noone can see that you zapped"
case .priv:
let pk = event.pubkey
let prof = state.profiles.lookup(id: pk)
let name = Profile.displayName(profile: prof, pubkey: pk)
return String(format: "Only '%@' can see that you zapped them",
name)
case .non_zap:
return "No zaps are sent, only a lightning payment."
}
}
var ZapTypePicker: some View { var ZapTypePicker: some View {
Picker(NSLocalizedString("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send."), selection: $zap_type) { Picker(NSLocalizedString("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send."), selection: $zap_type) {
Text("Public", comment: "Picker option to indicate that a zap should be sent publicly and identify the user as who sent it.").tag(ZapType.pub) Text("Public", comment: "Picker option to indicate that a zap should be sent publicly and identify the user as who sent it.").tag(ZapType.pub)
Text("Anonymous", comment: "Picker option to indicate that a zap should be sent anonymously and not identify the user as who sent it.").tag(ZapType.anon) Text("Private", comment: "Picker option to indicate that a zap should be sent privately and not identify the user to the public.").tag(ZapType.priv)
Text("Non-Zap", comment: "Picker option to indicate that sats should be sent to the user's wallet as a regular Lightning payment, not as a zap.").tag(ZapType.non_zap) Text("Anon", comment: "Picker option to indicate that a zap should be sent anonymously and not identify the user as who sent it.").tag(ZapType.anon)
Text("None", comment: "Picker option to indicate that sats should be sent to the user's wallet as a regular Lightning payment, not as a zap.").tag(ZapType.non_zap)
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
} }
@@ -180,15 +199,17 @@ struct CustomizeZapView: View {
}, header: { }, header: {
Text("Comment", comment: "Header text to indicate that the text field below it is a comment that will be used to send as part of a zap to the user.") Text("Comment", comment: "Header text to indicate that the text field below it is a comment that will be used to send as part of a zap to the user.")
}) })
Section(content: {
ZapTypePicker
}, header: {
Text("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send.")
})
} }
.dismissKeyboardOnTap() .dismissKeyboardOnTap()
Section(content: {
ZapTypePicker
}, header: {
Text("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send.")
}, footer: {
Text(zap_type_desc)
})
if zapping { if zapping {
Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.") Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.")

View File

@@ -21,7 +21,7 @@ struct ZapsView: View {
LazyVStack { LazyVStack {
ForEach(model.zaps, id: \.event.id) { zap in ForEach(model.zaps, id: \.event.id) { zap in
ZapEvent(damus: state, zap: zap) ZapEvent(damus: state, zap: zap)
.padding() .padding([.horizontal])
} }
} }
} }

View File

@@ -17,7 +17,32 @@ final class ZapTests: XCTestCase {
override func tearDownWithError() throws { override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class. // Put teardown code here. This method is called after the invocation of each test method in the class.
} }
func test_private_zap() throws {
let alice = generate_new_keypair().to_full()!
let bob = generate_new_keypair().to_full()!
let target = ZapTarget.profile(bob.pubkey)
let message = "hey bob!"
let zapreq = make_zap_request_event(keypair: alice, content: message, relays: [], target: target, zap_type: .priv)
XCTAssertNotNil(zapreq)
guard let zapreq else {
return
}
let decrypted = decrypt_private_zap(our_privkey: bob.privkey, zapreq: zapreq, target: target)
XCTAssertNotNil(decrypted)
guard let decrypted else {
return
}
XCTAssertEqual(zapreq.content, "")
XCTAssertEqual(decrypted.pubkey, alice.pubkey)
XCTAssertEqual(message, decrypted.content)
}
func testZap() throws { func testZap() throws {
let zapjson = "eyJpZCI6IjUzNmJlZTllODNjODE4ZTNiODJjMTAxOTM1MTI4YWUyN2EwZDQyOTAwMzlhYWYyNTNlZmU1ZjA5MjMyYzE5NjIiLCJwdWJrZXkiOiI5NjMwZjQ2NGNjYTZhNTE0N2FhOGEzNWYwYmNkZDNjZTQ4NTMyNGU3MzJmZDM5ZTA5MjMzYjFkODQ4MjM4ZjMxIiwiY3JlYXRlZF9hdCI6MTY3NDIwNDUzNSwia2luZCI6OTczNSwidGFncyI6W1sicCIsIjMyZTE4Mjc2MzU0NTBlYmIzYzVhN2QxMmMxZjhlN2IyYjUxNDQzOWFjMTBhNjdlZWYzZDlmZDljNWM2OGUyNDUiXSxbImJvbHQxMSIsImxuYmMxMHUxcDN1NTR0bnNwNTcyOXF2eG5renRqamtkNTg1eW4wbDg2MzBzMm01eDZsNTZ3eXk0ZWMybnU4eHV6NjI5eHFwcDV2MnE3aHVjNGpwamgwM2Z4OHVqZXQ1Nms3OWd4cXg3bWUycGV2ejZqMms4dDhtNGxnNXZxaHA1eWc1MDU3OGNtdWoyNG1mdDNxcnNybWd3ZjMwa2U3YXY3ZDc3Z2FtZmxkazlrNHNmMzltcXhxeWp3NXFjcXBqcnpqcTJoeWVoNXEzNmx3eDZ6dHd5cmw2dm1tcnZ6NnJ1ZndqZnI4N3lremZuYXR1a200dWRzNHl6YWszc3FxOW1jcXFxcXFxcWxncXFxcTg2cXF5ZzlxeHBxeXNncWFkeWVjdmR6ZjI3MHBkMzZyc2FmbDA3azQ1ZmNqMnN5OGU1djJ0ZW5kNTB2OTU3NnV4cDNkdmp6amV1aHJlODl5cGdjbTkwZDZsbTAwNGszMHlqNGF2NW1jc3M1bnl4NHU5bmVyOWdwcHY2eXF3Il0sWyJkZXNjcmlwdGlvbiIsIntcImlkXCI6XCJiMDkyMTYzNGIxYmI4ZWUzNTg0YmJiZjJlOGQ3OTBhZDk4NTk5ZDhlMDhmODFjNzAwZGRiZTQ4MjAxNTY4Yjk3XCIsXCJwdWJrZXlcIjpcIjdmYTU2ZjVkNjk2MmFiMWUzY2Q0MjRlNzU4YzMwMDJiODY2NWY3YjBkOGRjZWU5ZmU5ZTI4OGQ3NzUxYWMxOTRcIixcImNyZWF0ZWRfYXRcIjoxNjc0MjA0NTMxLFwia2luZFwiOjk3MzQsXCJ0YWdzXCI6W1tcInBcIixcIjMyZTE4Mjc2MzU0NTBlYmIzYzVhN2QxMmMxZjhlN2IyYjUxNDQzOWFjMTBhNjdlZWYzZDlmZDljNWM2OGUyNDVcIl0sW1wicmVsYXlzXCIsXCJ3c3M6Ly9yZWxheS5zbm9ydC5zb2NpYWxcIixcIndzczovL3JlbGF5LmRhbXVzLmlvXCIsXCJ3c3M6Ly9ub3N0ci1wdWIud2VsbG9yZGVyLm5ldFwiLFwid3NzOi8vbm9zdHIudjBsLmlvXCIsXCJ3c3M6Ly9wcml2YXRlLW5vc3RyLnYwbC5pb1wiLFwid3NzOi8vbm9zdHIuemViZWRlZS5jbG91ZFwiLFwid3NzOi8vcmVsYXkubm9zdHIuaW5mby9cIl1dLFwiY29udGVudFwiOlwiXCIsXCJzaWdcIjpcImQwODQwNGU2MjVmOWM1NjMzYWZhZGQxMWMxMTBiYTg4ZmNkYjRiOWUwOTJiOTg0MGU3NDgyYThkNTM3YjFmYzExODY5MmNmZDEzMWRkODMzNTM2NDc2OWE2NzE3NTRhZDdhYTk3MzEzNjgzYTRhZDdlZmI3NjQ3NmMwNGU1ZjE3XCJ9Il0sWyJwcmVpbWFnZSIsIjNlMDJhM2FmOGM4YmNmMmEzNzUzYzg3ZjMxMTJjNjU2YTIwMTE0ZWUwZTk4ZDgyMTliYzU2ZjVlOGE3MjM1YjMiXV0sImNvbnRlbnQiOiIiLCJzaWciOiIzYWI0NGQwZTIyMjhiYmQ0ZDIzNDFjM2ZhNzQwOTZjZmY2ZjU1Y2ZkYTk5YTVkYWRjY2Y0NWM2NjQ2MzdlMjExNTFiMmY5ZGQwMDQwZjFhMjRlOWY4Njg2NzM4YjE2YmY4MTM0YmRiZTQxYTIxOGM5MTFmN2JiMzFlNTk1NzhkMSJ9Cg==" let zapjson = "eyJpZCI6IjUzNmJlZTllODNjODE4ZTNiODJjMTAxOTM1MTI4YWUyN2EwZDQyOTAwMzlhYWYyNTNlZmU1ZjA5MjMyYzE5NjIiLCJwdWJrZXkiOiI5NjMwZjQ2NGNjYTZhNTE0N2FhOGEzNWYwYmNkZDNjZTQ4NTMyNGU3MzJmZDM5ZTA5MjMzYjFkODQ4MjM4ZjMxIiwiY3JlYXRlZF9hdCI6MTY3NDIwNDUzNSwia2luZCI6OTczNSwidGFncyI6W1sicCIsIjMyZTE4Mjc2MzU0NTBlYmIzYzVhN2QxMmMxZjhlN2IyYjUxNDQzOWFjMTBhNjdlZWYzZDlmZDljNWM2OGUyNDUiXSxbImJvbHQxMSIsImxuYmMxMHUxcDN1NTR0bnNwNTcyOXF2eG5renRqamtkNTg1eW4wbDg2MzBzMm01eDZsNTZ3eXk0ZWMybnU4eHV6NjI5eHFwcDV2MnE3aHVjNGpwamgwM2Z4OHVqZXQ1Nms3OWd4cXg3bWUycGV2ejZqMms4dDhtNGxnNXZxaHA1eWc1MDU3OGNtdWoyNG1mdDNxcnNybWd3ZjMwa2U3YXY3ZDc3Z2FtZmxkazlrNHNmMzltcXhxeWp3NXFjcXBqcnpqcTJoeWVoNXEzNmx3eDZ6dHd5cmw2dm1tcnZ6NnJ1ZndqZnI4N3lremZuYXR1a200dWRzNHl6YWszc3FxOW1jcXFxcXFxcWxncXFxcTg2cXF5ZzlxeHBxeXNncWFkeWVjdmR6ZjI3MHBkMzZyc2FmbDA3azQ1ZmNqMnN5OGU1djJ0ZW5kNTB2OTU3NnV4cDNkdmp6amV1aHJlODl5cGdjbTkwZDZsbTAwNGszMHlqNGF2NW1jc3M1bnl4NHU5bmVyOWdwcHY2eXF3Il0sWyJkZXNjcmlwdGlvbiIsIntcImlkXCI6XCJiMDkyMTYzNGIxYmI4ZWUzNTg0YmJiZjJlOGQ3OTBhZDk4NTk5ZDhlMDhmODFjNzAwZGRiZTQ4MjAxNTY4Yjk3XCIsXCJwdWJrZXlcIjpcIjdmYTU2ZjVkNjk2MmFiMWUzY2Q0MjRlNzU4YzMwMDJiODY2NWY3YjBkOGRjZWU5ZmU5ZTI4OGQ3NzUxYWMxOTRcIixcImNyZWF0ZWRfYXRcIjoxNjc0MjA0NTMxLFwia2luZFwiOjk3MzQsXCJ0YWdzXCI6W1tcInBcIixcIjMyZTE4Mjc2MzU0NTBlYmIzYzVhN2QxMmMxZjhlN2IyYjUxNDQzOWFjMTBhNjdlZWYzZDlmZDljNWM2OGUyNDVcIl0sW1wicmVsYXlzXCIsXCJ3c3M6Ly9yZWxheS5zbm9ydC5zb2NpYWxcIixcIndzczovL3JlbGF5LmRhbXVzLmlvXCIsXCJ3c3M6Ly9ub3N0ci1wdWIud2VsbG9yZGVyLm5ldFwiLFwid3NzOi8vbm9zdHIudjBsLmlvXCIsXCJ3c3M6Ly9wcml2YXRlLW5vc3RyLnYwbC5pb1wiLFwid3NzOi8vbm9zdHIuemViZWRlZS5jbG91ZFwiLFwid3NzOi8vcmVsYXkubm9zdHIuaW5mby9cIl1dLFwiY29udGVudFwiOlwiXCIsXCJzaWdcIjpcImQwODQwNGU2MjVmOWM1NjMzYWZhZGQxMWMxMTBiYTg4ZmNkYjRiOWUwOTJiOTg0MGU3NDgyYThkNTM3YjFmYzExODY5MmNmZDEzMWRkODMzNTM2NDc2OWE2NzE3NTRhZDdhYTk3MzEzNjgzYTRhZDdlZmI3NjQ3NmMwNGU1ZjE3XCJ9Il0sWyJwcmVpbWFnZSIsIjNlMDJhM2FmOGM4YmNmMmEzNzUzYzg3ZjMxMTJjNjU2YTIwMTE0ZWUwZTk4ZDgyMTliYzU2ZjVlOGE3MjM1YjMiXV0sImNvbnRlbnQiOiIiLCJzaWciOiIzYWI0NGQwZTIyMjhiYmQ0ZDIzNDFjM2ZhNzQwOTZjZmY2ZjU1Y2ZkYTk5YTVkYWRjY2Y0NWM2NjQ2MzdlMjExNTFiMmY5ZGQwMDQwZjFhMjRlOWY4Njg2NzM4YjE2YmY4MTM0YmRiZTQxYTIxOGM5MTFmN2JiMzFlNTk1NzhkMSJ9Cg=="
@@ -33,7 +58,7 @@ final class ZapTests: XCTestCase {
return return
} }
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: "9630f464cca6a5147aa8a35f0bcdd3ce485324e732fd39e09233b1d848238f31") else { guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: "9630f464cca6a5147aa8a35f0bcdd3ce485324e732fd39e09233b1d848238f31", our_privkey: nil) else {
XCTAssert(false) XCTAssert(false)
return return
} }