ndb: switch to nostrdb notes

This is a refactor of the codebase to use a more memory-efficient
representation of notes. It should also be much faster at decoding since
we're using a custom C json parser now.

Changelog-Changed: Improved memory usage and performance when processing events
This commit is contained in:
William Casarin
2023-07-26 08:46:44 -07:00
parent 55bbe8f855
commit cebd1f48ca
110 changed files with 2153 additions and 1799 deletions

View File

@@ -30,9 +30,9 @@ class Contacts {
func set_mutelist(_ ev: NostrEvent) {
let oldlist = self.mutelist
self.mutelist = ev
let old = Set(oldlist?.referenced_pubkeys.map({ $0.ref_id }) ?? [])
let new = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
let old = oldlist.map({ ev in Set(ev.referenced_pubkeys) }) ?? Set<Pubkey>()
let new = Set(ev.referenced_pubkeys)
let diff = old.symmetricDifference(new)
var new_mutes = Set<Pubkey>()
@@ -40,14 +40,14 @@ class Contacts {
for d in diff {
if new.contains(d) {
new_mutes.insert(d.string())
new_mutes.insert(d)
} else {
new_unmutes.insert(d.string())
new_unmutes.insert(d)
}
}
// TODO: set local mutelist here
self.muted = Set(ev.referenced_pubkeys.map({ $0.ref_id.string() }))
self.muted = Set(ev.referenced_pubkeys)
if new_mutes.count > 0 {
notify(.new_mutes(new_mutes))
@@ -72,7 +72,7 @@ class Contacts {
func get_followed_hashtags() -> Set<String> {
guard let ev = self.event else { return Set() }
return Set(ev.referenced_hashtags.map({ $0.ref_id.string() }))
return Set(ev.referenced_hashtags.map({ $0.hashtag }))
}
func add_friend_pubkey(_ pubkey: Pubkey) {
@@ -81,8 +81,7 @@ class Contacts {
func add_friend_contact(_ contact: NostrEvent) {
friends.insert(contact.pubkey)
for tag in contact.referenced_pubkeys {
let pk = tag.id.string()
for pk in contact.referenced_pubkeys {
friend_of_friends.insert(pk)
// Exclude themself and us.
@@ -122,7 +121,7 @@ class Contacts {
}
}
func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: ReferencedId) -> NostrEvent? {
func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else {
return nil
}
@@ -132,7 +131,7 @@ func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeyp
return ev
}
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: ReferencedId) -> NostrEvent? {
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
guard let cs = our_contacts else {
return nil
}
@@ -146,14 +145,9 @@ func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: Fu
return ev
}
func unfollow_reference_event(our_contacts: NostrEvent, keypair: FullKeypair, unfollow: ReferencedId) -> NostrEvent? {
func unfollow_reference_event(our_contacts: NostrEvent, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
let tags = our_contacts.tags.reduce(into: [[String]]()) { ts, tag in
if tag.count >= 2,
let fst = unfollow.key.first,
let afst = AsciiCharacter(fst),
tag[0].matches_char(afst),
tag[1].matches_str(unfollow.ref_id)
{
if let tag = FollowRef.from_tag(tag: tag), tag == unfollow {
return
}
@@ -165,7 +159,7 @@ func unfollow_reference_event(our_contacts: NostrEvent, keypair: FullKeypair, un
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: Array(tags))
}
func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow: ReferencedId) -> NostrEvent? {
func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
guard let cs = our_contacts else {
// don't create contacts for now so we don't nuke our contact list due to connectivity issues
// we should only create contacts during profile creation
@@ -219,12 +213,20 @@ func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [String: R
return relay_info
}
func is_already_following(contacts: NostrEvent, follow: ReferencedId) -> Bool {
guard let key = follow.key.first_char() else { return false }
return contacts.references(id: follow.ref_id, key: key)
func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
return contacts.references.contains { ref in
switch (ref, follow) {
case let (.hashtag(ht), .hashtag(follow_ht)):
return ht.string() == follow_ht
case let (.pubkey(pk), .pubkey(follow_pk)):
return pk == follow_pk
case (.hashtag, .pubkey), (.pubkey, .hashtag),
(.event, _), (.quote, _), (.param, _):
return false
}
}
}
func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEvent, follow: ReferencedId) -> NostrEvent? {
func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEvent, follow: FollowRef) -> NostrEvent? {
// don't update if we're already following
if is_already_following(contacts: our_contacts, follow: follow) {
return nil
@@ -233,7 +235,7 @@ func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEven
let kind = NostrKind.contacts.rawValue
var tags = our_contacts.tags.strings()
tags.append(refid_to_tag(follow))
tags.append(follow.tag)
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags)
}

View File

@@ -16,14 +16,6 @@ class CreateAccountModel: ObservableObject {
@Published var privkey: Privkey = .empty
@Published var profile_image: URL? = nil
var pubkey_bech32: String {
return bech32_pubkey(self.pubkey) ?? ""
}
var privkey_bech32: String {
return bech32_privkey(self.privkey) ?? ""
}
var rendered_name: String {
if real_name.isEmpty {
return nick_name

View File

@@ -42,8 +42,8 @@ struct DamusState {
// thread zaps
if let ev = zap.event, !settings.nozaps, zap.is_in_thread {
// [nozaps]: thread zaps are only available outside of the app store
replies.count_replies(ev)
events.add_replies(ev: ev)
replies.count_replies(ev, privkey: self.keypair.privkey)
events.add_replies(ev: ev, privkey: self.keypair.privkey)
}
// associate with events as well

View File

@@ -8,19 +8,17 @@
import Foundation
enum EventRef: Equatable {
case mention(Mention)
case thread_id(ReferencedId)
case reply(ReferencedId)
case reply_to_root(ReferencedId)
var is_mention: Mention? {
if case .mention(let m) = self {
return m
}
case mention(Mention<NoteRef>)
case thread_id(NoteRef)
case reply(NoteRef)
case reply_to_root(NoteRef)
var is_mention: NoteRef? {
if case .mention(let m) = self { return m.ref }
return nil
}
var is_direct_reply: ReferencedId? {
var is_direct_reply: NoteRef? {
switch self {
case .mention:
return nil
@@ -33,7 +31,7 @@ enum EventRef: Equatable {
}
}
var is_thread_id: ReferencedId? {
var is_thread_id: NoteRef? {
switch self {
case .mention:
return nil
@@ -46,7 +44,7 @@ enum EventRef: Equatable {
}
}
var is_reply: ReferencedId? {
var is_reply: NoteRef? {
switch self {
case .mention:
return nil
@@ -64,10 +62,8 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
return blocks.reduce(into: []) { acc, block in
switch block {
case .mention(let m):
if m.type == type {
if let idx = m.index {
acc.insert(idx)
}
if m.ref.key == type, let idx = m.index {
acc.insert(idx)
}
case .relay:
return
@@ -83,7 +79,7 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
}
}
func interp_event_refs_without_mentions(_ refs: [ReferencedId]) -> [EventRef] {
func interp_event_refs_without_mentions(_ refs: [NoteRef]) -> [EventRef] {
if refs.count == 0 {
return []
}
@@ -105,16 +101,15 @@ func interp_event_refs_without_mentions(_ refs: [ReferencedId]) -> [EventRef] {
return evrefs
}
func interp_event_refs_with_mentions(tags: [[String]], mention_indices: Set<Int>) -> [EventRef] {
func interp_event_refs_with_mentions(tags: Tags, mention_indices: Set<Int>) -> [EventRef] {
var mentions: [EventRef] = []
var ev_refs: [ReferencedId] = []
var ev_refs: [NoteRef] = []
var i: Int = 0
for tag in tags {
if tag.count >= 2 && tag[0] == "e" {
let ref = tag_to_refid(tag)!
if let ref = NoteRef.from_tag(tag: tag) {
if mention_indices.contains(i) {
let mention = Mention(index: i, type: .event, ref: ref)
let mention = Mention<NoteRef>(index: i, ref: ref)
mentions.append(.mention(mention))
} else {
ev_refs.append(ref)
@@ -128,18 +123,17 @@ func interp_event_refs_with_mentions(tags: [[String]], mention_indices: Set<Int>
return replies
}
func interpret_event_refs(blocks: [Block], tags: [[String]]) -> [EventRef] {
func interpret_event_refs(blocks: [Block], tags: Tags) -> [EventRef] {
if tags.count == 0 {
return []
}
/// build a set of indices for each event mention
let mention_indices = build_mention_indices(blocks, type: .event)
let mention_indices = build_mention_indices(blocks, type: .e)
/// simpler case with no mentions
if mention_indices.count == 0 {
let ev_refs = get_referenced_ids(tags: tags, key: "e")
return interp_event_refs_without_mentions(ev_refs)
return interp_event_refs_without_mentions_ndb(References<NoteRef>(tags: tags))
}
return interp_event_refs_with_mentions(tags: tags, mention_indices: mention_indices)

View File

@@ -41,14 +41,11 @@ class EventsModel: ObservableObject {
}
private func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == kind.rawValue else {
guard ev.kind == kind.rawValue,
ev.referenced_ids.last == target else {
return
}
guard ev.referenced_ids.last?.ref_id.string() == target else {
return
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
objectWillChange.send()
}

View File

@@ -10,7 +10,11 @@ import Foundation
enum FollowTarget {
case pubkey(Pubkey)
case contact(NostrEvent)
var follow_ref: FollowRef {
FollowRef.pubkey(pubkey)
}
var pubkey: Pubkey {
switch self {
case .pubkey(let pk): return pk

View File

@@ -36,7 +36,7 @@ class FollowersModel: ObservableObject {
func subscribe() {
let filter = get_filter()
let filters = [filter]
print_filters(relay_id: "following", filters: [filters])
//print_filters(relay_id: "following", filters: [filters])
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
}

View File

@@ -39,7 +39,7 @@ class FollowingModel {
return
}
let filters = [filter]
print_filters(relay_id: "following", filters: [filters])
//print_filters(relay_id: "following", filters: [filters])
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
}

View File

@@ -25,20 +25,17 @@ struct NewEventsBits: OptionSet {
enum Resubscribe {
case following
case unfollowing(ReferencedId)
case unfollowing(FollowRef)
}
enum HomeResubFilter {
case pubkey(Pubkey)
case hashtag(String)
init?(from: ReferencedId) {
if from.key == "p" {
self = .pubkey(from.ref_id)
return
} else if from.key == "t" {
self = .hashtag(from.ref_id)
return
init?(from: FollowRef) {
switch from {
case .hashtag(let ht): self = .hashtag(ht.string())
case .pubkey(let pk): self = .pubkey(pk)
}
return nil
@@ -52,7 +49,9 @@ enum HomeResubFilter {
if contacts.is_friend(ev.pubkey) {
return false
}
return ev.references(id: ht, key: "t")
return ev.referenced_hashtags.contains(where: { ref_ht in
ht == ref_ht.hashtag
})
}
}
}
@@ -63,7 +62,6 @@ class HomeModel {
var damus_state: DamusState
var channels: [String: NostrEvent] = [:]
// NDBTODO: let's get rid of this entirely, let nostrdb handle it
var has_event: [String: Set<NoteId>] = [:]
var deleted_events: Set<NoteId> = Set()
@@ -183,10 +181,6 @@ class HomeModel {
handle_dm(ev)
case .delete:
handle_delete_event(ev)
case .channel_create:
handle_channel_create(ev)
case .channel_meta:
break
case .zap:
handle_zap_event(ev)
case .zap_request:
@@ -262,10 +256,6 @@ class HomeModel {
}
func handle_channel_create(_ ev: NostrEvent) {
self.channels[ev.id] = ev
}
func filter_events() {
events.filter { ev in
!damus_state.contacts.is_muted(ev.pubkey)
@@ -301,12 +291,11 @@ class HomeModel {
}
func handle_boost_event(sub_id: String, _ ev: NostrEvent) {
var boost_ev_id = ev.last_refid()?.ref_id
var boost_ev_id = ev.last_refid()
if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
boost_ev_id = inner_ev.id
Task {
guard validate_event(ev: inner_ev) == .ok else {
return
@@ -318,7 +307,6 @@ class HomeModel {
}
}
}
}
guard let e = boost_ev_id else {
@@ -345,14 +333,14 @@ class HomeModel {
return
}
switch damus_state.likes.add_event(ev, target: e.ref_id) {
switch damus_state.likes.add_event(ev, target: e) {
case .already_counted:
break
case .success(let n):
handle_notification(ev: ev)
let liked = Counted(event: ev, id: e.ref_id, total: n)
let liked = Counted(event: ev, id: e, total: n)
notify(.liked(liked))
notify(.update_stats(note_id: e.ref_id))
notify(.update_stats(note_id: e))
}
}
@@ -553,14 +541,10 @@ class HomeModel {
}
}
guard let name = get_referenced_ids(tags: ev.tags, key: "d").first else {
guard ev.referenced_params.contains(where: { p in p.param.matches_str("mute") }) else {
return
}
guard name.ref_id == "mute" else {
return
}
damus_state.contacts.set_mutelist(ev)
}
@@ -631,7 +615,7 @@ class HomeModel {
// TODO: will we need to process this in other places like zap request contents, etc?
process_image_metadatas(cache: damus_state.events, ev: ev)
damus_state.replies.count_replies(ev)
damus_state.replies.count_replies(ev, privkey: self.damus_state.keypair.privkey)
damus_state.events.insert(ev)
if sub_id == home_subid {
@@ -699,33 +683,26 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
let contacts = state.contacts
var new_refs = Set<ReferencedId>()
// our contacts
for tag in ev.tags {
guard let ref = tag_to_refid(tag) else { continue }
new_refs.insert(ref)
}
var old_refs = Set<ReferencedId>()
// find removed contacts
if let old_ev = m_old_ev {
for tag in old_ev.tags {
guard let ref = tag_to_refid(tag) else { continue }
old_refs.insert(ref)
}
}
let new_refs = Set<FollowRef>(ev.referenced_follows)
let old_refs = m_old_ev.map({ old_ev in Set(old_ev.referenced_follows) }) ?? Set()
let diff = new_refs.symmetricDifference(old_refs)
for ref in diff {
if new_refs.contains(ref) {
notify(.followed(ref))
if ref.key == "p" {
contacts.add_friend_pubkey(ref.ref_id)
switch ref {
case .pubkey(let pk):
contacts.add_friend_pubkey(pk)
case .hashtag:
// I guess I could cache followed hashtags here... whatever
break
}
} else {
notify(.unfollowed(ref))
if ref.key == "p" {
contacts.remove_friend(ref.ref_id)
switch ref {
case .pubkey(let pk):
contacts.remove_friend(pk)
case .hashtag: break
}
}
}
@@ -758,6 +735,7 @@ func abbrev_ids_field(_ n: String, _ ids: [String]?) -> String {
return "\(n): \(abbrev_ids(ids))"
}
/*
func print_filter(_ f: NostrFilter) {
let fmt = [
abbrev_ids_field("ids", f.ids),
@@ -783,6 +761,7 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
}
print("-----")
}
*/
func process_metadata_profile(our_pubkey: Pubkey, profiles: Profiles, profile: Profile, ev: NostrEvent) {
var old_nip05: String? = nil
@@ -1003,7 +982,7 @@ func handle_incoming_dm(debouncer: Debouncer?, ev: NostrEvent, our_pubkey: Pubke
var the_pk = ev.pubkey
if ours {
if let ref_pk = ev.referenced_pubkeys.first {
the_pk = ref_pk.ref_id
the_pk = ref_pk
} else {
// self dm!?
print("TODO: handle self dm?")
@@ -1123,14 +1102,8 @@ func handle_last_events(debouncer: Debouncer?, new_events: NewEventsBits, ev: No
/// Sometimes we get garbage in our notifications. Ensure we have our pubkey on this event
func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
for tag in ev.tags {
if tag.count >= 2 && tag[0] == "p" && tag[1] == our_pubkey {
return true
}
}
return false
func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: Pubkey) -> Bool {
return ev.referenced_pubkeys.contains(our_pubkey)
}
@@ -1185,7 +1158,7 @@ func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale
content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
content.sound = UNNotificationSound.default
content.userInfo = LossyLocalNotification(type: .profile_zap, event_id: profile_id).to_user_info()
content.userInfo = LossyLocalNotification(type: .profile_zap, mention: .pubkey(profile_id)).to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
@@ -1206,7 +1179,7 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale:
content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
content.sound = UNNotificationSound.default
content.userInfo = LossyLocalNotification(type: .zap, event_id: evId).to_user_info()
content.userInfo = LossyLocalNotification(type: .zap, mention: .note(evId)).to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
@@ -1264,19 +1237,26 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
return
}
if type == .text && damus_state.settings.mention_notification {
if type == .text, damus_state.settings.mention_notification {
let blocks = ev.blocks(damus_state.keypair.privkey).blocks
for case .mention(let mention) in blocks where mention.ref.ref_id == damus_state.keypair.pubkey {
for case .mention(let mention) in blocks {
guard case .pubkey(let pk) = mention.ref, pk == damus_state.keypair.pubkey else {
continue
}
let content_preview = render_notification_content_preview(cache: damus_state.events, ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content_preview)
create_local_notification(profiles: damus_state.profiles, notify: notify )
}
} else if type == .boost && damus_state.settings.repost_notification, let inner_ev = ev.get_inner_event(cache: damus_state.events) {
} else if type == .boost,
damus_state.settings.repost_notification,
let inner_ev = ev.get_inner_event(cache: damus_state.events)
{
let content_preview = render_notification_content_preview(cache: damus_state.events, ev: inner_ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: content_preview)
create_local_notification(profiles: damus_state.profiles, notify: notify)
} else if type == .like && damus_state.settings.like_notification,
let evid = ev.referenced_ids.last?.ref_id,
} else if type == .like,
damus_state.settings.like_notification,
let evid = ev.referenced_ids.last,
let liked_event = damus_state.events.lookup(evid)
{
let content_preview = render_notification_content_preview(cache: damus_state.events, ev: liked_event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
@@ -1335,22 +1315,35 @@ enum ProcessZapResult {
case failed
}
extension Sequence {
func just_one() -> Element? {
var got_one = false
var the_x: Element? = nil
for x in self {
guard !got_one else {
return nil
}
the_x = x
got_one = true
}
return the_x
}
}
// securely get the zap target's pubkey. this can be faked so we need to be
// careful
func get_zap_target_pubkey(ev: NostrEvent, events: EventCache) -> Pubkey? {
let etags = ev.referenced_ids
let etags = Array(ev.referenced_ids)
guard let etag = etags.first else {
// no etags, ptag-only case
let ptags = ev.referenced_pubkeys
// ensure that there is only 1 ptag to stop fake profile zap attacks
guard ptags.count == 1 else {
guard let a = ev.referenced_pubkeys.just_one() else {
return nil
}
return ptags.first?.id
// TODO: just return data here
return a
}
// we have an e-tag
@@ -1361,7 +1354,7 @@ func get_zap_target_pubkey(ev: NostrEvent, events: EventCache) -> Pubkey? {
}
// we can't trust the p tag on note zaps because they can be faked
return events.lookup(etag.id)?.pubkey
return events.lookup(etag)?.pubkey
}
func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @escaping (ProcessZapResult) -> Void) {

View File

@@ -7,31 +7,93 @@
import Foundation
enum MentionType {
case pubkey
case event
enum MentionType: AsciiCharacter, TagKey {
case p
case e
var ref: String {
var keychar: AsciiCharacter {
self.rawValue
}
}
enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
case pubkey(Pubkey) // TODO: handle nprofile
case note(NoteId)
var key: MentionType {
switch self {
case .pubkey:
return "p"
case .event:
return "e"
case .pubkey: return .p
case .note: return .e
}
}
var bech32: String {
switch self {
case .pubkey(let pubkey): return bech32_pubkey(pubkey)
case .note(let noteId): return bech32_note_id(noteId)
}
}
static func from_bech32(str: String) -> MentionRef? {
switch Bech32Object.parse(str) {
case .note(let noteid): return .note(noteid)
case .npub(let pubkey): return .pubkey(pubkey)
default: return nil
}
}
var pubkey: Pubkey? {
switch self {
case .pubkey(let pubkey): return pubkey
case .note: return nil
}
}
var tag: [String] {
switch self {
case .pubkey(let pubkey): return ["p", pubkey.hex()]
case .note(let noteId): return ["e", noteId.hex()]
}
}
static func from_tag(tag: TagSequence) -> MentionRef? {
guard tag.count >= 2 else { return nil }
var i = tag.makeIterator()
guard let t0 = i.next(),
let chr = t0.single_char,
let mention_type = MentionType(rawValue: chr),
let id = i.next()?.id()
else {
return nil
}
switch mention_type {
case .p: return .pubkey(Pubkey(id))
case .e: return .note(NoteId(id))
}
}
}
struct Mention: Equatable {
struct Mention<T: Equatable>: Equatable {
let index: Int?
let type: MentionType
let ref: ReferencedId
let ref: T
static func note(_ id: String) -> Mention {
return Mention(index: nil, type: .event, ref: .e(id))
static func any(_ mention_id: MentionRef, index: Int? = nil) -> Mention<MentionRef> {
return Mention<MentionRef>(index: index, ref: mention_id)
}
static func pubkey(_ pubkey: String) -> Mention {
return Mention(index: nil, type: .pubkey, ref: .p(pubkey))
static func noteref(_ id: NoteRef, index: Int? = nil) -> Mention<NoteRef> {
return Mention<NoteRef>(index: index, ref: id)
}
static func note(_ id: NoteId, index: Int? = nil) -> Mention<NoteId> {
return Mention<NoteId>(index: index, ref: id)
}
static func pubkey(_ pubkey: Pubkey, index: Int? = nil) -> Mention<Pubkey> {
return Mention<Pubkey>(index: index, ref: pubkey)
}
}
@@ -80,7 +142,7 @@ enum Block: Equatable {
}
case text(String)
case mention(Mention)
case mention(Mention<MentionRef>)
case hashtag(String)
case url(URL)
case invoice(Invoice)
@@ -116,14 +178,14 @@ enum Block: Equatable {
}
var is_note_mention: Bool {
guard case .mention(let mention) = self else {
return false
if case .mention(let mention) = self,
case .note = mention.ref {
return true
}
return mention.type == .event
return false
}
var is_mention: Mention? {
var is_mention: Mention<MentionRef>? {
if case .mention(let m) = self {
return m
}
@@ -137,12 +199,11 @@ func render_blocks(blocks: [Block]) -> String {
case .mention(let m):
if let idx = m.index {
return str + "#[\(idx)]"
} else if m.type == .pubkey, let pk = bech32_pubkey(m.ref.ref_id) {
return str + "nostr:\(pk)"
} else if let note_id = bech32_note_id(m.ref.ref_id) {
return str + "nostr:\(note_id)"
} else {
return str + m.ref.ref_id
}
switch m.ref {
case .pubkey(let pk): return str + "nostr:\(pk.npub)"
case .note(let note_id): return str + "nostr:\(note_id.bech32)"
}
case .relay(let relay):
return str + relay
@@ -163,43 +224,13 @@ struct Blocks: Equatable {
let blocks: [Block]
}
func parse_note_content(content: String, tags: [[String]]) -> Blocks {
var out: [Block] = []
var bs = note_blocks()
bs.num_blocks = 0;
blocks_init(&bs)
let bytes = content.utf8CString
let _ = bytes.withUnsafeBufferPointer { p in
damus_parse_content(&bs, p.baseAddress)
}
var i = 0
while (i < bs.num_blocks) {
let block = bs.blocks[i]
if let converted = convert_block(block, tags: tags) {
out.append(converted)
}
i += 1
}
let words = Int(bs.words)
blocks_free(&bs)
return Blocks(words: words, blocks: out)
}
func strblock_to_string(_ s: str_block_t) -> String? {
let len = s.end - s.start
let bytes = Data(bytes: s.start, count: len)
return String(bytes: bytes, encoding: .utf8)
}
func convert_block(_ b: block_t, tags: [[String]]) -> Block? {
func convert_block(_ b: block_t, tags: TagsSequence?) -> Block? {
if b.type == BLOCK_HASHTAG {
guard let str = strblock_to_string(b.block.str) else {
return nil
@@ -211,7 +242,7 @@ func convert_block(_ b: block_t, tags: [[String]]) -> Block? {
}
return .text(str)
} else if b.type == BLOCK_MENTION_INDEX {
return convert_mention_index_block(ind: b.block.mention_index, tags: tags)
return convert_mention_index_block(ind: Int(b.block.mention_index), tags: tags)
} else if b.type == BLOCK_URL {
return convert_url_block(b.block.str)
} else if b.type == BLOCK_INVOICE {
@@ -321,41 +352,29 @@ func convert_mention_bech32_block(_ b: mention_bech32_block) -> Block?
switch b.bech32.type {
case NOSTR_BECH32_NOTE:
let note = b.bech32.data.note;
let event_id = hex_encode(Data(bytes: note.event_id, count: 32))
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: nil, key: "e")
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
let note_id = NoteId(Data(bytes: note.event_id, count: 32))
return .mention(.any(.note(note_id)))
case NOSTR_BECH32_NEVENT:
let nevent = b.bech32.data.nevent;
let event_id = hex_encode(Data(bytes: nevent.event_id, count: 32))
var relay_id: String? = nil
if nevent.relays.num_relays > 0 {
relay_id = strblock_to_string(nevent.relays.relays.0)
}
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: relay_id, key: "e")
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
let note_id = NoteId(Data(bytes: nevent.event_id, count: 32))
return .mention(.any(.note(note_id)))
case NOSTR_BECH32_NPUB:
let npub = b.bech32.data.npub
let pubkey = hex_encode(Data(bytes: npub.pubkey, count: 32))
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
let pubkey = Pubkey(Data(bytes: npub.pubkey, count: 32))
return .mention(.any(.pubkey(pubkey)))
case NOSTR_BECH32_NSEC:
let nsec = b.bech32.data.nsec
let nsec_bytes = Data(bytes: nsec.nsec, count: 32)
let pubkey = privkey_to_pubkey_raw(sec: nsec_bytes.bytes) ?? hex_encode(nsec_bytes)
return .mention(.pubkey(pubkey))
let privkey = Privkey(Data(bytes: nsec.nsec, count: 32))
guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil }
return .mention(.any(.pubkey(pubkey)))
case NOSTR_BECH32_NPROFILE:
let nprofile = b.bech32.data.nprofile
let pubkey = hex_encode(Data(bytes: nprofile.pubkey, count: 32))
var relay_id: String? = nil
if nprofile.relays.num_relays > 0 {
relay_id = strblock_to_string(nprofile.relays.relays.0)
}
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: relay_id, key: "p")
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
let pubkey = Pubkey(Data(bytes: nprofile.pubkey, count: 32))
return .mention(.any(.pubkey(pubkey)))
case NOSTR_BECH32_NRELAY:
let nrelay = b.bech32.data.nrelay
@@ -388,24 +407,22 @@ func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
return nil
}
func convert_mention_index_block(ind: Int32, tags: [[String]]) -> Block?
func convert_mention_index_block(ind: Int, tags: TagsSequence?) -> Block?
{
let ind = Int(ind)
if ind < 0 || (ind + 1 > tags.count) || tags[ind].count < 2 {
guard let tags,
ind >= 0,
ind + 1 <= tags.count
else {
return .text("#[\(ind)]")
}
let tag = tags[ind]
guard let mention_type = parse_mention_type(tag[0]) else {
guard let mention = MentionRef.from_tag(tag: tag) else {
return .text("#[\(ind)]")
}
guard let ref = tag_to_refid(tag) else {
return .text("#[\(ind)]")
}
return .mention(Mention(index: ind, type: mention_type, ref: ref))
return .mention(.any(mention, index: ind))
}
func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? {
@@ -427,25 +444,6 @@ struct PostTags {
let tags: [[String]]
}
func parse_mention_type_ndb(_ tag: NdbTagElem) -> MentionType? {
if tag.matches_char("e") {
return .event
} else if tag.matches_char("p") {
return .pubkey
}
return nil
}
func parse_mention_type(_ c: String) -> MentionType? {
if c == "e" {
return .event
} else if c == "p" {
return .pubkey
}
return nil
}
/// Convert
func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
var new_tags = tags
@@ -453,12 +451,11 @@ func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
for post_block in post_blocks {
switch post_block {
case .mention(let mention):
let mention_type = mention.type
if mention_type == .event {
if case .note = mention.ref {
continue
}
new_tags.append(refid_to_tag(mention.ref))
new_tags.append(mention.ref.tag)
case .hashtag(let hashtag):
new_tags.append(["t", hashtag.lowercased()])
case .text: break
@@ -474,7 +471,7 @@ func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
}
func post_to_event(post: NostrPost, keypair: FullKeypair) -> NostrEvent? {
let tags = post.references.map(refid_to_tag) + post.tags
let tags = post.references.map({ r in r.tag }) + post.tags
let post_blocks = parse_post_blocks(content: post.content)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
let content = render_blocks(blocks: post_tags.blocks)

View File

@@ -13,7 +13,11 @@ fileprivate func getMutedThreadsKey(pubkey: Pubkey) -> String {
func loadMutedThreads(pubkey: Pubkey) -> [NoteId] {
let key = getMutedThreadsKey(pubkey: pubkey)
return UserDefaults.standard.stringArray(forKey: key) ?? []
let xs = UserDefaults.standard.stringArray(forKey: key) ?? []
return xs.reduce(into: [NoteId]()) { ids, k in
guard let note_id = hex_decode(k) else { return }
ids.append(NoteId(Data(note_id)))
}
}
func saveMutedThreads(pubkey: Pubkey, currentValue: [NoteId], value: [NoteId]) -> Bool {

View File

@@ -193,17 +193,15 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
private func insert_reaction(_ ev: NostrEvent) -> Bool {
guard let ref_id = ev.referenced_ids.last else {
guard let id = ev.referenced_ids.last else {
return false
}
let id = ref_id.id
if let evgrp = self.reactions[id.string()] {
if let evgrp = self.reactions[id] {
return evgrp.insert(ev)
} else {
let evgrp = EventGroup()
self.reactions[id.string()] = evgrp
self.reactions[id] = evgrp
return evgrp.insert(ev)
}
}

View File

@@ -10,10 +10,10 @@ import Foundation
struct NostrPost {
let kind: NostrKind
let content: String
let references: [ReferencedId]
let references: [RefId]
let tags: [[String]]
init(content: String, references: [ReferencedId], kind: NostrKind = .text, tags: [[String]] = []) {
init(content: String, references: [RefId], kind: NostrKind = .text, tags: [[String]] = []) {
self.content = content
self.references = references
self.kind = kind
@@ -21,96 +21,9 @@ struct NostrPost {
}
}
func parse_post_mention_type(_ p: Parser) -> MentionType? {
if parse_char(p, "@") {
return .pubkey
}
if parse_char(p, "&") {
return .event
}
return nil
}
func parse_post_reference(_ p: Parser) -> ReferencedId? {
let start = p.pos
guard let typ = parse_post_mention_type(p) else {
return parse_nostr_ref_uri(p)
}
if let ref = parse_post_mention(p, mention_type: typ) {
return ref
}
p.pos = start
return nil
}
func is_bech32_char(_ c: Character) -> Bool {
let contains = "qpzry9x8gf2tvdw0s3jn54khce6mua7l".contains(c)
return contains
}
func parse_post_mention(_ p: Parser, mention_type: MentionType) -> ReferencedId? {
if let id = parse_hexstr(p, len: 64) {
return ReferencedId(ref_id: id, relay_id: nil, key: mention_type.ref)
} else if let bech32_ref = parse_post_bech32_mention(p) {
return bech32_ref
} else {
return nil
}
}
// TODO: replace this with our C parser
func parse_post_bech32_mention(_ p: Parser) -> ReferencedId? {
let start = p.pos
if parse_str(p, "note") {
} else if parse_str(p, "npub") {
} else if parse_str(p, "nsec") {
} else {
return nil
}
if !parse_char(p, "1") {
p.pos = start
return nil
}
guard consume_until(p, match: { c in !is_bech32_char(c) }, end_ok: true) else {
return nil
}
let end = p.pos
let sliced = String(substring(p.str, start: start, end: end))
guard let decoded = try? bech32_decode(sliced) else {
p.pos = start
return nil
}
let hex = hex_encode(decoded.data)
switch decoded.hrp {
case "note":
return ReferencedId(ref_id: hex, relay_id: nil, key: "e")
case "npub":
return ReferencedId(ref_id: hex, relay_id: nil, key: "p")
case "nsec":
guard let pubkey = privkey_to_pubkey(privkey: hex) else {
p.pos = start
return nil
}
return ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")
default:
p.pos = start
return nil
}
}
/// Return a list of tags
func parse_post_blocks(content: String) -> [Block] {
return parse_note_content(content: content, tags: []).blocks
return parse_note_content(content: .content(content, nil)).blocks
}

View File

@@ -6,34 +6,3 @@
//
import Foundation
enum PostBlock {
case text(String)
case ref(ReferencedId)
case hashtag(String)
var is_text: String? {
if case .text(let txt) = self {
return txt
}
return nil
}
var is_hashtag: String? {
if case .hashtag(let ht) = self {
return ht
}
return nil
}
var is_ref: ReferencedId? {
if case .ref(let ref) = self {
return ref
}
return nil
}
}
func parse_post_textblock(str: String, from: Int, to: Int) -> PostBlock {
return .text(String(substring(str, start: from, end: to)))
}

View File

@@ -33,17 +33,8 @@ class ProfileModel: ObservableObject, Equatable {
guard let contacts = self.contacts else {
return false
}
for tag in contacts.tags {
guard tag.count >= 2,
tag[0].matches_char("p"),
tag[1].matches_str(pubkey)
else {
continue
}
}
return false
return contacts.referenced_pubkeys.contains(pubkey)
}
func get_follow_target() -> FollowTarget {
@@ -77,7 +68,7 @@ class ProfileModel: ObservableObject, Equatable {
text_filter.limit = 500
print("subscribing to profile \(pubkey) with sub_id \(sub_id)")
print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
damus.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
damus.pool.subscribe(sub_id: prof_subid, filters: [profile_filter], handler: handle_event)
}

View File

@@ -12,21 +12,20 @@ struct ReplyDesc {
let others: Int
}
func make_reply_description(_ tags: [[String]]) -> ReplyDesc {
func make_reply_description(_ tags: Tags) -> ReplyDesc {
var c = 0
var ns: [Pubkey] = []
var i = tags.count - 1
while i >= 0 {
let tag = tags[i]
if tag.count >= 2 && tag[0] == "p" {
var i = tags.count
for tag in tags {
if let pk = Pubkey.from_tag(tag: tag) {
c += 1
if ns.count < 2 {
ns.append(tag[1])
ns.append(pk)
}
}
i -= 1
}
return ReplyDesc(pubkeys: ns, others: c)
}

View File

@@ -68,10 +68,6 @@ class SearchModel: ObservableObject {
let (sub_id, done) = handle_subid_event(pool: state.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
if ev.is_textlike && ev.should_show_event {
self.add_event(ev)
} else if ev.known_kind == .channel_create {
// unimplemented
} else if ev.known_kind == .channel_meta {
// unimplemented
}
}
@@ -89,16 +85,16 @@ class SearchModel: ObservableObject {
func event_matches_hashtag(_ ev: NostrEvent, hashtags: [String]) -> Bool {
for tag in ev.tags {
if tag_is_hashtag(tag) && hashtags.contains(tag[1]) {
if tag_is_hashtag(tag) && hashtags.contains(tag[1].string()) {
return true
}
}
return false
}
func tag_is_hashtag(_ tag: [String]) -> Bool {
func tag_is_hashtag(_ tag: Tag) -> Bool {
// "hashtag" is deprecated, will remove in the future
return tag.count >= 2 && (tag[0] == "hashtag" || tag[0] == "t")
return tag.count >= 2 && tag[0].matches_char("t")
}
func event_matches_filter(_ ev: NostrEvent, filter: NostrFilter) -> Bool {

View File

@@ -18,7 +18,7 @@ class ThreadModel: ObservableObject {
self.event_map = Set()
self.event = event
self.original_event = event
add_event(event)
add_event(event, privkey: damus_state.keypair.privkey)
}
var is_original: Bool {
@@ -46,10 +46,10 @@ class ThreadModel: ObservableObject {
}
@discardableResult
func set_active_event(_ ev: NostrEvent) -> Bool {
func set_active_event(_ ev: NostrEvent, privkey: Privkey?) -> Bool {
self.event = ev
add_event(ev)
add_event(ev, privkey: privkey)
//self.objectWillChange.send()
return false
}
@@ -85,15 +85,15 @@ class ThreadModel: ObservableObject {
damus_state.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event)
}
func add_event(_ ev: NostrEvent) {
func add_event(_ ev: NostrEvent, privkey: Privkey?) {
if event_map.contains(ev) {
return
}
let the_ev = damus_state.events.upsert(ev)
damus_state.replies.count_replies(the_ev)
damus_state.events.add_replies(ev: the_ev)
damus_state.replies.count_replies(the_ev, privkey: privkey)
damus_state.events.add_replies(ev: the_ev, privkey: privkey)
event_map.insert(ev)
objectWillChange.send()
}
@@ -112,7 +112,7 @@ class ThreadModel: ObservableObject {
}
} else if ev.is_textlike {
self.add_event(ev)
self.add_event(ev, privkey: damus_state.keypair.privkey)
}
}

View File

@@ -61,31 +61,35 @@ class UserSearchCache {
return
}
var petnames: [String: String] = [:]
// Gets all petnames from our new contacts list.
newEvent.tags.forEach { tag in
guard tag.count >= 4 && tag[0] == "p" else {
var petnames: [Pubkey: String] = [:]
for tag in newEvent.tags {
guard tag.count > 3,
let chr = tag[0].single_char, chr == "p",
let id = tag[1].id()
else {
return
}
let pubkey = tag[1]
let petname = tag[3]
let pubkey = Pubkey(id)
petnames[pubkey] = petname
petnames[pubkey] = tag[3].string()
}
// Compute the diff with the old contacts list, if it exists,
// mark the ones that are the same to not be removed from the user search cache,
// and remove the old ones that are different from the user search cache.
if let oldEvent, oldEvent.known_kind == .contacts && oldEvent.pubkey == id {
oldEvent.tags.forEach { tag in
guard tag.count >= 4 && tag[0] == "p" else {
if let oldEvent, oldEvent.known_kind == .contacts, oldEvent.pubkey == id {
for tag in oldEvent.tags {
guard tag.count >= 4,
tag[0].matches_char("p"),
let id = tag[1].id()
else {
return
}
let pubkey = tag[1]
let oldPetname = tag[3]
let pubkey = Pubkey(id)
let oldPetname = tag[3].string()
if let newPetname = petnames[pubkey] {
if newPetname.caseInsensitiveCompare(oldPetname) == .orderedSame {

View File

@@ -56,15 +56,10 @@ class ZapsModel: ObservableObject {
let events = state.events.lookup_zaps(target: target).map { $0.request.ev }
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
case .event(_, let ev):
guard ev.kind == 9735 else {
return
}
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
return
}
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else {
guard ev.kind == 9735,
let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey),
let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey)
else {
return
}