Implement NIP04: Encrypted Direct Messages

Closes #5

This adds encrypted direct message support to damus

Changelog-Added: Implement NIP04: Encrypted Direct Messages
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2022-06-30 07:16:48 -07:00
parent 0744156c0c
commit c122035851
24 changed files with 892 additions and 228 deletions

View File

@@ -204,13 +204,13 @@ func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] {
}
// TODO: tests for this
func is_friend_event(_ ev: NostrEvent, our_pubkey: String, contacts: Contacts) -> Bool
func is_friend_event(_ ev: NostrEvent, keypair: Keypair, contacts: Contacts) -> Bool
{
if !contacts.is_friend(ev.pubkey) {
return false
}
if !ev.is_reply {
if ev.is_reply(keypair.privkey) {
return true
}

View File

@@ -0,0 +1,15 @@
//
// DirectMessagesModel.swift
// damus
//
// Created by William Casarin on 2022-06-29.
//
import Foundation
class DirectMessagesModel: ObservableObject {
@Published var events: [(String, [NostrEvent])] = []
@Published var loading: Bool = false
}

View File

@@ -147,8 +147,8 @@ func interpret_event_refs(blocks: [Block], tags: [[String]]) -> [EventRef] {
}
func event_is_reply(_ ev: NostrEvent) -> Bool {
return ev.event_refs.contains { evref in
func event_is_reply(_ ev: NostrEvent, privkey: String?) -> Bool {
return ev.event_refs(privkey).contains { evref in
return evref.is_reply != nil
}
}

View File

@@ -9,91 +9,102 @@ import Foundation
struct NewEventsBits {
let bits: Int
init() {
bits = 0
}
init (prev: NewEventsBits, setting: Timeline) {
self.bits = prev.bits | timeline_bit(setting)
}
init (prev: NewEventsBits, unsetting: Timeline) {
self.bits = prev.bits & ~timeline_bit(unsetting)
}
func is_set(_ timeline: Timeline) -> Bool {
let notification_bit = timeline_bit(timeline)
return (bits & notification_bit) == notification_bit
}
}
class HomeModel: ObservableObject {
var damus_state: DamusState
var has_event: [String: Set<String>] = [:]
var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
var done_init: Bool = false
let home_subid = UUID().description
let contacts_subid = UUID().description
let notifications_subid = UUID().description
let dms_subid = UUID().description
let init_subid = UUID().description
@Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications: [NostrEvent] = []
@Published var dms: [(String, [NostrEvent])] = []
@Published var events: [NostrEvent] = []
@Published var loading: Bool = false
@Published var signal: SignalModel = SignalModel()
init() {
self.damus_state = DamusState.empty
}
init(damus_state: DamusState) {
self.damus_state = damus_state
}
var pool: RelayPool {
return damus_state.pool
}
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
if !has_event.keys.contains(sub_id) {
has_event[sub_id] = Set()
return false
}
return has_event[sub_id]!.contains(ev_id)
}
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
return
}
let last_k = get_last_event_of_kind(relay_id: relay_id, kind: ev.kind)
if last_k == nil || ev.created_at > last_k!.created_at {
last_event_of_kind[relay_id]?[ev.kind] = ev
}
if ev.kind == 1 {
guard let kind = ev.known_kind else {
return
}
switch kind {
case .text:
handle_text_event(sub_id: sub_id, ev)
} else if ev.kind == 0 {
handle_metadata_event(ev)
} else if ev.kind == 6 {
handle_boost_event(sub_id: sub_id, ev)
} else if ev.kind == 7 {
handle_like_event(ev)
} else if ev.kind == 3 {
case .contacts:
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
case .metadata:
handle_metadata_event(ev)
case .boost:
handle_boost_event(sub_id: sub_id, ev)
case .like:
handle_like_event(ev)
case .dm:
handle_dm(ev)
case .delete:
break
}
}
func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) {
process_contact_event(pool: damus_state.pool, contacts: damus_state.contacts, pubkey: damus_state.pubkey, ev: ev)
if sub_id == init_subid {
pool.send(.unsubscribe(init_subid), to: [relay_id])
if !done_init {
@@ -102,23 +113,23 @@ class HomeModel: ObservableObject {
}
}
}
func handle_boost_event(sub_id: String, _ ev: NostrEvent) {
var boost_ev_id = ev.last_refid()?.ref_id
// CHECK SIGS ON THESE
if let inner_ev = ev.inner_event {
boost_ev_id = inner_ev.id
if inner_ev.kind == 1 {
handle_text_event(sub_id: sub_id, ev)
}
}
guard let e = boost_ev_id else {
return
}
switch self.damus_state.boosts.add_event(ev, target: e) {
case .already_counted:
break
@@ -127,15 +138,15 @@ class HomeModel: ObservableObject {
notify(.boosted, boosted)
}
}
func handle_like_event(_ ev: NostrEvent) {
guard let e = ev.last_refid() else {
// no id ref? invalid like event
return
}
// CHECK SIGS ON THESE
switch damus_state.likes.add_event(ev, target: e.ref_id) {
case .already_counted:
break
@@ -144,8 +155,8 @@ class HomeModel: ObservableObject {
notify(.liked, liked)
}
}
func handle_event(relay_id: String, conn_event: NostrConnectionEvent) {
switch conn_event {
case .ws_event(let ev):
@@ -156,7 +167,7 @@ class HomeModel: ObservableObject {
self.events.insert(wsev, at: 0)
}
*/
switch ev {
case .connected:
@@ -182,7 +193,7 @@ class HomeModel: ObservableObject {
default:
break
}
update_signal_from_pool(signal: self.signal, pool: self.pool)
print("ws_event \(ev)")
@@ -191,44 +202,58 @@ class HomeModel: ObservableObject {
switch ev {
case .event(let sub_id, let ev):
// globally handle likes
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata
if !always_process {
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
return
}
self.process_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
case .notice(let msg):
//self.events.insert(NostrEvent(content: "NOTICE from \(relay_id): \(msg)", pubkey: "system"), at: 0)
print(msg)
case .eose:
self.loading = false
break
}
}
}
/// Send the initial filters, just our contact list mostly
func send_initial_filters(relay_id: String) {
var filter = NostrFilter.filter_contacts
filter.authors = [self.damus_state.pubkey]
filter.limit = 1
pool.send(.subscribe(.init(filters: [filter], sub_id: init_subid)), to: [relay_id])
}
func send_home_filters(relay_id: String?) {
// TODO: since times should be based on events from a specific relay
// perhaps we could mark this in the relay pool somehow
var friends = damus_state.contacts.get_friend_list()
friends.append(damus_state.pubkey)
var contacts_filter = NostrFilter.filter_kinds([0])
contacts_filter.authors = friends
var dms_filter = NostrFilter.filter_kinds([
NostrKind.dm.rawValue,
])
var our_dms_filter = NostrFilter.filter_kinds([
NostrKind.dm.rawValue,
])
// friends only?...
//dms_filter.authors = friends
dms_filter.limit = 500
dms_filter.pubkeys = [ damus_state.pubkey ]
our_dms_filter.authors = [ damus_state.pubkey ]
// TODO: separate likes?
var home_filter = NostrFilter.filter_kinds([
NostrKind.text.rawValue,
@@ -238,7 +263,7 @@ class HomeModel: ObservableObject {
// include our pubkey as well even if we're not technically a friend
home_filter.authors = friends
home_filter.limit = 500
var notifications_filter = NostrFilter.filter_kinds([
NostrKind.text.rawValue,
NostrKind.like.rawValue,
@@ -250,56 +275,60 @@ class HomeModel: ObservableObject {
var home_filters = [home_filter]
var notifications_filters = [notifications_filter]
var contacts_filters = [contacts_filter]
var dms_filters = [dms_filter, our_dms_filter]
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
home_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: home_filters)
contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters)
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters])
dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
if let relay_id = relay_id {
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id])
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: [relay_id])
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: [relay_id])
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: [relay_id])
} else {
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)))
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)))
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)))
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)))
}
}
func handle_metadata_event(_ ev: NostrEvent) {
process_metadata_event(profiles: damus_state.profiles, ev: ev)
}
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
guard let m = last_event_of_kind[relay_id] else {
last_event_of_kind[relay_id] = [:]
return nil
}
return m[kind]
}
func handle_last_event(ev: NostrEvent, timeline: Timeline) {
let last_ev = get_last_event(timeline)
if last_ev == nil || last_ev!.created_at < ev.created_at {
save_last_event(ev, timeline: timeline)
new_events = NewEventsBits(prev: new_events, setting: timeline)
}
}
func handle_notification(ev: NostrEvent) {
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
return
}
handle_last_event(ev: ev, timeline: .notifications)
}
func insert_home_event(_ ev: NostrEvent) -> Bool {
let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at })
if ok {
@@ -307,24 +336,67 @@ class HomeModel: ObservableObject {
}
return ok
}
func should_hide_event(_ ev: NostrEvent) -> Bool {
return false
}
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
if should_hide_event(ev) {
return
}
if sub_id == home_subid {
if is_friend_event(ev, our_pubkey: damus_state.pubkey, contacts: damus_state.contacts) {
if is_friend_event(ev, keypair: damus_state.keypair, contacts: damus_state.contacts) {
let _ = insert_home_event(ev)
}
} else if sub_id == notifications_subid {
handle_notification(ev: ev)
}
}
func handle_dm(_ ev: NostrEvent) {
var inserted = false
var found = false
let ours = ev.pubkey == self.damus_state.pubkey
var i = 0
var the_pk = ev.pubkey
if ours {
if let ref_pk = ev.referenced_pubkeys.first {
the_pk = ref_pk.ref_id
} else {
// self dm!?
print("TODO: handle self dm?")
}
}
for (pk, _) in dms {
if pk == the_pk {
found = true
inserted = insert_uniq_sorted_event(events: &(dms[i].1), new_ev: ev) {
$0.created_at < $1.created_at
}
break
}
i += 1
}
if !found {
inserted = true
dms.append((the_pk, [ev]))
}
if inserted {
handle_last_event(ev: ev, timeline: .dms)
dms = dms.sorted { a, b in
a.1.last!.created_at > b.1.last!.created_at
}
}
}
}
@@ -332,7 +404,7 @@ func update_signal_from_pool(signal: SignalModel, pool: RelayPool) {
if signal.max_signal != pool.relays.count {
signal.max_signal = pool.relays.count
}
if signal.signal != pool.num_connecting {
signal.signal = signal.max_signal - pool.num_connecting
}
@@ -342,7 +414,7 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
if !contacts.is_friend(ev.pubkey) {
return
}
contacts.add_friend_contact(ev)
}
@@ -350,9 +422,9 @@ func load_our_contacts(contacts: Contacts, our_pubkey: String, ev: NostrEvent) {
guard ev.pubkey == our_pubkey else {
return
}
contacts.event = ev
// our contacts
for tag in ev.tags {
if tag.count > 1 && tag[0] == "p" {
@@ -398,7 +470,7 @@ func print_filter(_ f: NostrFilter) {
abbrev_field("until", f.until),
abbrev_field("limit", f.limit)
].filter({ !$0.isEmpty }).joined(separator: ",")
print("Filter(\(fmt))")
}
@@ -427,7 +499,7 @@ func process_metadata_event(profiles: Profiles, ev: NostrEvent) {
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at)
profiles.add(id: ev.pubkey, profile: tprof)
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
@@ -441,11 +513,11 @@ func load_our_relays(our_pubkey: String, pool: RelayPool, ev: NostrEvent) {
guard ev.pubkey == our_pubkey else {
return
}
guard let decoded = decode_json_relays(ev.content) else {
return
}
for key in decoded.keys {
if let url = URL(string: key) {
if let _ = try? pool.add_relay(url, info: decoded[key]!) {
@@ -460,11 +532,11 @@ func remove_bootstrap_nodes(_ damus_state: DamusState) {
guard let contacts = damus_state.contacts.event else {
return
}
guard let relays = decode_json_relays(contacts.content) else {
return
}
let descriptors = relays.reduce(into: []) { arr, kv in
guard let url = URL(string: kv.key) else {
return

View File

@@ -30,6 +30,8 @@ enum InitialEvent {
/// manages the lifetime of a thread
class ThreadModel: ObservableObject {
let privkey: String?
let kind: Int
@Published var initial_event: InitialEvent
@Published var events: [NostrEvent] = []
@Published var event_map: [String: Int] = [:]
@@ -54,14 +56,25 @@ class ThreadModel: ObservableObject {
let pool: RelayPool
var sub_id = UUID().description
init(evid: String, pool: RelayPool) {
init(evid: String, pool: RelayPool, privkey: String?) {
self.pool = pool
self.initial_event = .event_id(evid)
self.privkey = privkey
self.kind = NostrKind.text.rawValue
}
init(event: NostrEvent, pool: RelayPool) {
init(event: NostrEvent, pool: RelayPool, privkey: String?) {
self.pool = pool
self.initial_event = .event(event)
self.privkey = privkey
self.kind = NostrKind.text.rawValue
}
init(event: NostrEvent, pool: RelayPool, privkey: String?, kind: Int) {
self.pool = pool
self.initial_event = .event(event)
self.privkey = privkey
self.kind = kind
}
func unsubscribe() {
@@ -89,7 +102,7 @@ class ThreadModel: ObservableObject {
return true
}
func set_active_event(_ ev: NostrEvent) {
func set_active_event(_ ev: NostrEvent, privkey: String?) {
if should_resubscribe(ev) {
unsubscribe()
self.initial_event = .event(ev)
@@ -97,14 +110,14 @@ class ThreadModel: ObservableObject {
} else {
self.initial_event = .event(ev)
if events.count == 0 {
add_event(ev)
add_event(ev, privkey: privkey)
}
}
}
func subscribe() {
var ref_events = NostrFilter.filter_kinds([1,5,6,7])
var events_filter = NostrFilter.filter_kinds([1])
var ref_events = NostrFilter.filter_kinds([self.kind,5,6,7])
var events_filter = NostrFilter.filter_kinds([self.kind])
//var likes_filter = NostrFilter.filter_kinds(7])
// TODO: add referenced relays
@@ -134,12 +147,12 @@ class ThreadModel: ObservableObject {
return nil
}
func add_event(_ ev: NostrEvent) {
func add_event(_ ev: NostrEvent, privkey: String?) {
if event_map[ev.id] != nil {
return
}
for reply in ev.direct_replies() {
for reply in ev.direct_replies(privkey) {
self.replies.add(id: ev.id, reply_id: reply.ref_id)
}
@@ -158,7 +171,7 @@ class ThreadModel: ObservableObject {
if let evid = self.initial_event.is_event_id {
if ev.id == evid {
// this should trigger a resubscribe...
set_active_event(ev)
set_active_event(ev, privkey: privkey)
}
}
@@ -167,7 +180,7 @@ class ThreadModel: ObservableObject {
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
let done = handle_subid_event(pool: pool, sub_id: sub_id, relay_id: relay_id, ev: ev) { ev in
if ev.known_kind == .text {
self.add_event(ev)
self.add_event(ev, privkey: self.privkey)
}
}