1
0
mirror of git://jb55.com/damus synced 2024-09-30 00:40:45 +00:00

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

@ -95,6 +95,7 @@
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */; };
4C28A4122A6D03D200C1A7A5 /* ReferencedId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C28A4112A6D03D200C1A7A5 /* ReferencedId.swift */; };
4C2B10282A7B0F5C008AA43E /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B10272A7B0F5C008AA43E /* Log.swift */; };
4C2B7BF22A71B6540049DEE7 /* Id.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B7BF12A71B6540049DEE7 /* Id.swift */; };
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; };
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */; };
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */; };
@ -229,7 +230,6 @@
4CA352A02A76AE80003BB08B /* Notify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA3529F2A76AE80003BB08B /* Notify.swift */; };
4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA352A12A76AEC5003BB08B /* LikedNotify.swift */; };
4CA352A42A76AFF3003BB08B /* UpdateStatsNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA352A32A76AFF3003BB08B /* UpdateStatsNotify.swift */; };
4CA352A62A76B020003BB08B /* Pubkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA352A52A76B020003BB08B /* Pubkey.swift */; };
4CA352A82A76B37E003BB08B /* NewMutesNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA352A72A76B37E003BB08B /* NewMutesNotify.swift */; };
4CA352AA2A76BF3A003BB08B /* LocalNotificationNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA352A92A76BF3A003BB08B /* LocalNotificationNotify.swift */; };
4CA352AC2A76C07F003BB08B /* NewUnmutesNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA352AB2A76C07F003BB08B /* NewUnmutesNotify.swift */; };
@ -268,6 +268,10 @@
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
4CC14FEF2A73FCCB007AEB17 /* IdType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FEE2A73FCCB007AEB17 /* IdType.swift */; };
4CC14FF12A73FCDB007AEB17 /* Pubkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF02A73FCDB007AEB17 /* Pubkey.swift */; };
4CC14FF52A740BB7007AEB17 /* NoteId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF42A740BB7007AEB17 /* NoteId.swift */; };
4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF82A741939007AEB17 /* Referenced.swift */; };
4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC6193929DC777C006A86D1 /* RelayBootstrap.swift */; };
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; };
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
@ -589,6 +593,7 @@
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveKeysView.swift; sourceTree = "<group>"; };
4C28A4112A6D03D200C1A7A5 /* ReferencedId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferencedId.swift; sourceTree = "<group>"; };
4C2B10272A7B0F5C008AA43E /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
4C2B7BF12A71B6540049DEE7 /* Id.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Id.swift; sourceTree = "<group>"; };
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
4C30AC7329A5680900E2BD5A /* EventGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupView.swift; sourceTree = "<group>"; };
@ -762,7 +767,6 @@
4CA3529F2A76AE80003BB08B /* Notify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notify.swift; sourceTree = "<group>"; };
4CA352A12A76AEC5003BB08B /* LikedNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikedNotify.swift; sourceTree = "<group>"; };
4CA352A32A76AFF3003BB08B /* UpdateStatsNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateStatsNotify.swift; sourceTree = "<group>"; };
4CA352A52A76B020003BB08B /* Pubkey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pubkey.swift; sourceTree = "<group>"; };
4CA352A72A76B37E003BB08B /* NewMutesNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMutesNotify.swift; sourceTree = "<group>"; };
4CA352A92A76BF3A003BB08B /* LocalNotificationNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationNotify.swift; sourceTree = "<group>"; };
4CA352AB2A76C07F003BB08B /* NewUnmutesNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewUnmutesNotify.swift; sourceTree = "<group>"; };
@ -808,6 +812,10 @@
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
4CC14FEE2A73FCCB007AEB17 /* IdType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdType.swift; sourceTree = "<group>"; };
4CC14FF02A73FCDB007AEB17 /* Pubkey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pubkey.swift; sourceTree = "<group>"; };
4CC14FF42A740BB7007AEB17 /* NoteId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteId.swift; sourceTree = "<group>"; };
4CC14FF82A741939007AEB17 /* Referenced.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Referenced.swift; sourceTree = "<group>"; };
4CC6193929DC777C006A86D1 /* RelayBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayBootstrap.swift; sourceTree = "<group>"; };
4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; };
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
@ -1309,7 +1317,7 @@
4C363A8F28247A1D006E126D /* NostrLink.swift */,
50088DA029E8271A008A1FDF /* WebSocket.swift */,
4C28A4112A6D03D200C1A7A5 /* ReferencedId.swift */,
4CA352A52A76B020003BB08B /* Pubkey.swift */,
4C2B7BF12A71B6540049DEE7 /* Id.swift */,
);
path = Nostr;
sourceTree = "<group>";
@ -1555,6 +1563,25 @@
path = Profile;
sourceTree = "<group>";
};
4CC14FEC2A73FC9A007AEB17 /* Types */ = {
isa = PBXGroup;
children = (
4CC14FED2A73FCBB007AEB17 /* Ids */,
);
path = Types;
sourceTree = "<group>";
};
4CC14FED2A73FCBB007AEB17 /* Ids */ = {
isa = PBXGroup;
children = (
4CC14FEE2A73FCCB007AEB17 /* IdType.swift */,
4CC14FF02A73FCDB007AEB17 /* Pubkey.swift */,
4CC14FF42A740BB7007AEB17 /* NoteId.swift */,
4CC14FF82A741939007AEB17 /* Referenced.swift */,
);
path = Ids;
sourceTree = "<group>";
};
4CC7AAEE297F11B300430951 /* Events */ = {
isa = PBXGroup;
children = (
@ -1652,6 +1679,7 @@
children = (
4C1D4FB32A7967990024F453 /* build-git-hash.txt */,
4CA3529C2A76AE47003BB08B /* Notify */,
4CC14FEC2A73FC9A007AEB17 /* Types */,
F7F0BA23297892AE009531F3 /* Modifiers */,
4C4A3A5A288A1B2200453788 /* damus.entitlements */,
4CE4F9DF285287A000C00DD9 /* Components */,
@ -1684,6 +1712,7 @@
4CE6DEF627F7A08200C66700 /* damusTests */ = {
isa = PBXGroup;
children = (
4C0379642A7BFE8E0037B8C4 /* Events */,
4C9B0DEC2A65A74000CBDA21 /* Util */,
4C0C03962A61E2670098B3B8 /* Fixtures */,
4C7D097D2A0C58B900943473 /* WalletConnectTests.swift */,
@ -2087,6 +2116,7 @@
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
4CC14FF12A73FCDB007AEB17 /* Pubkey.swift in Sources */,
4CA9275D2A28FF630098A105 /* LongformView.swift in Sources */,
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */,
@ -2124,7 +2154,6 @@
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
4C5D5C992A6AF8F80024563C /* NdbNote.swift in Sources */,
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
4CA352A62A76B020003BB08B /* Pubkey.swift in Sources */,
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */,
@ -2180,6 +2209,7 @@
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */,
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */,
4CC14FEF2A73FCCB007AEB17 /* IdType.swift in Sources */,
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
@ -2197,6 +2227,7 @@
4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */,
4CA352A42A76AFF3003BB08B /* UpdateStatsNotify.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
4C2B7BF22A71B6540049DEE7 /* Id.swift in Sources */,
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */,
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */,
@ -2271,6 +2302,7 @@
4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */,
4C1253542A76C7D60004F4B8 /* LogoutNotify.swift in Sources */,
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
4CC14FF52A740BB7007AEB17 /* NoteId.swift in Sources */,
4C19AE512A5CEF7C00C90DB7 /* NostrScript.swift in Sources */,
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
@ -2294,6 +2326,7 @@
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */,
4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
@ -2701,7 +2734,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@ -2750,7 +2783,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;

View File

@ -51,12 +51,12 @@ struct SearchHeaderView: View {
func unfollow(_ hashtag: String) {
is_following = false
handle_unfollow(state: state, unfollow: .t(hashtag))
handle_unfollow(state: state, unfollow: FollowRef.hashtag(hashtag))
}
func follow(_ hashtag: String) {
is_following = true
handle_follow(state: state, follow: .t(hashtag))
handle_follow(state: state, follow: .hashtag(hashtag))
}
func FollowButton(_ ht: String) -> some View {
@ -104,15 +104,20 @@ struct SearchHeaderView: View {
}
}
func hashtag_matches_search(desc: DescribedSearch, ref: ReferencedId) -> Bool {
guard let ht = desc.is_hashtag, ref.key == "t" && ref.ref_id == ht
else { return false }
func hashtag_matches_search(desc: DescribedSearch, ref: FollowRef) -> Bool {
guard case .hashtag(let follow_ht) = ref,
case .hashtag(let search_ht) = desc,
follow_ht == search_ht
else {
return false
}
return true
}
func is_following_hashtag(contacts: NostrEvent?, hashtag: String) -> Bool {
guard let contacts else { return false }
return is_already_following(contacts: contacts, follow: .t(hashtag))
return is_already_following(contacts: contacts, follow: .hashtag(hashtag))
}

View File

@ -143,7 +143,7 @@ func translate_note(profiles: Profiles, privkey: Privkey?, event: NostrEvent, se
}
// Render translated note
let translated_blocks = event.get_blocks(content: translated_note)
let translated_blocks = parse_note_content(content: .content(translated_note, event.tags))
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles)
// and cache it

View File

@ -7,75 +7,56 @@
import Foundation
func tag_to_refid_ndb(_ tag: TagSequence) -> ReferencedId? {
guard tag.count >= 2 else { return nil }
enum NoteContent {
case note(NostrEvent)
case content(String, TagsSequence?)
let key = tag[0].string()
let ref_id = tag[1].string()
var relay_id: String? = nil
if tag.count >= 3 {
relay_id = tag[2].string()
init(note: NostrEvent, privkey: Privkey?) {
if note.known_kind == .dm {
self = .content(note.get_content(privkey), note.tags)
} else {
self = .note(note)
}
}
return ReferencedId(ref_id: ref_id, relay_id: relay_id, key: key)
}
func convert_mention_index_block_ndb(ind: Int, tags: TagsSequence) -> Block? {
if ind < 0 || (ind + 1 > tags.count) || tags[ind]!.count < 2 {
return .text("#[\(ind)]")
}
guard let tag = tags[ind], let fst = tag.first(where: { _ in true }) else {
return nil
}
guard let mention_type = parse_mention_type_ndb(fst) else {
return .text("#[\(ind)]")
}
guard let ref = tag_to_refid_ndb(tag) else {
return .text("#[\(ind)]")
}
return .mention(Mention(index: ind, type: mention_type, ref: ref))
}
func convert_block_ndb(_ b: block_t, tags: TagsSequence) -> Block? {
if b.type == BLOCK_MENTION_INDEX {
return convert_mention_index_block_ndb(ind: Int(b.block.mention_index), tags: tags)
}
return convert_block(b, tags: [])
}
func parse_note_content_ndb(note: NdbNote) -> Blocks {
func parsed_blocks_finish(bs: inout note_blocks, tags: TagsSequence?) -> Blocks {
var out: [Block] = []
var bs = note_blocks()
bs.num_blocks = 0;
blocks_init(&bs)
damus_parse_content(&bs, note.content_raw)
var i = 0
while (i < bs.num_blocks) {
let block = bs.blocks[i]
if let converted = convert_block_ndb(block, tags: note.tags) {
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 parse_note_content(content: NoteContent) -> Blocks {
var bs = note_blocks()
bs.num_blocks = 0;
blocks_init(&bs)
switch content {
case .content(let s, let tags):
return s.withCString { cptr in
damus_parse_content(&bs, cptr)
return parsed_blocks_finish(bs: &bs, tags: tags)
}
case .note(let note):
damus_parse_content(&bs, note.content_raw)
return parsed_blocks_finish(bs: &bs, tags: note.tags)
}
}
func interpret_event_refs_ndb(blocks: [Block], tags: TagsSequence) -> [EventRef] {
@ -84,38 +65,37 @@ func interpret_event_refs_ndb(blocks: [Block], tags: TagsSequence) -> [EventRef]
}
/// 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 = References.ids(tags: tags)
return interp_event_refs_without_mentions_ndb(ev_refs)
return interp_event_refs_without_mentions_ndb(tags.note.referenced_noterefs)
}
return interp_event_refs_with_mentions_ndb(tags: tags, mention_indices: mention_indices)
}
func interp_event_refs_without_mentions_ndb(_ ev_tags: LazyFilterSequence<References>) -> [EventRef] {
func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> [EventRef] {
var count = 0
var evrefs: [EventRef] = []
var first: Bool = true
var first_ref: Reference? = nil
var first_ref: NoteRef? = nil
for ref in ev_tags {
if first {
first_ref = ref
evrefs.append(.thread_id(ref.to_referenced_id()))
evrefs.append(.thread_id(ref))
first = false
} else {
evrefs.append(.reply(ref.to_referenced_id()))
evrefs.append(.reply(ref))
}
count += 1
}
if let first_ref, count == 1 {
let r = first_ref.to_referenced_id()
let r = first_ref
return [.reply_to_root(r)]
}
@ -124,19 +104,15 @@ func interp_event_refs_without_mentions_ndb(_ ev_tags: LazyFilterSequence<Refere
func interp_event_refs_with_mentions_ndb(tags: TagsSequence, 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].matches_char("e"),
let ref = tag_to_refid_ndb(tag)
{
if let note_id = NoteRef.from_tag(tag: tag) {
if mention_indices.contains(i) {
let mention = Mention(index: i, type: .event, ref: ref)
mentions.append(.mention(mention))
mentions.append(.mention(.noteref(note_id, index: i)))
} else {
ev_refs.append(ref)
ev_refs.append(note_id)
}
}
i += 1

View File

@ -43,9 +43,9 @@ enum Sheets: Identifiable {
var id: String {
switch self {
case .report: return "report"
case .post(let action): return "post-" + (action.ev?.id ?? "")
case .event(let ev): return "event-" + ev.id
case .zap(let sheet): return "zap-" + sheet.target.id
case .post(let action): return "post-" + (action.ev?.id.hex() ?? "")
case .event(let ev): return "event-" + ev.id.hex()
case .zap(let sheet): return "zap-" + hex_encode(sheet.target.id)
case .select_wallet: return "select-wallet"
case .filter: return "filter"
case .suggestedUsers: return "suggested-users"
@ -397,14 +397,14 @@ struct ContentView: View {
}
.onReceive(handle_notify(.unfollow)) { target in
guard let state = self.damus_state else { return }
_ = handle_unfollow_notif(state: state, target: target)
_ = handle_unfollow(state: state, unfollow: target.follow_ref)
}
.onReceive(handle_notify(.unfollowed)) { unfollow in
home.resubscribe(.unfollowing(unfollow))
}
.onReceive(handle_notify(.follow)) { target in
guard let state = self.damus_state else { return }
guard handle_follow_notif(state: state, target: target) else { return }
handle_follow_notif(state: state, target: target)
}
.onReceive(handle_notify(.followed)) { _ in
home.resubscribe(.following)
@ -469,26 +469,29 @@ struct ContentView: View {
.onReceive(handle_notify(.local_notification)) { local in
guard let damus_state else { return }
if local.type == .profile_zap {
open_profile(pubkey: local.event_id)
return
}
guard let target = damus_state.events.lookup(local.event_id) else {
return
}
switch local.type {
case .dm:
selected_timeline = .dms
damus_state.dms.set_active_dm(target.pubkey)
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
case .like, .zap, .mention, .repost:
open_event(ev: target)
case .profile_zap:
// Handled separately above.
break
switch local.mention {
case .pubkey(let pubkey):
open_profile(pubkey: pubkey)
case .note(let noteId):
guard let target = damus_state.events.lookup(noteId) else {
return
}
switch local.type {
case .dm:
selected_timeline = .dms
damus_state.dms.set_active_dm(target.pubkey)
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
case .like, .zap, .mention, .repost:
open_event(ev: target)
case .profile_zap:
// Handled separately above.
break
}
}
}
.onReceive(handle_notify(.onlyzaps_mode)) { hide in
home.filter_events()
@ -529,7 +532,7 @@ struct ContentView: View {
guard let ds = damus_state,
let keypair = ds.keypair.to_full(),
let pubkey = muting,
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: pubkey)
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(pubkey))
else {
return
}
@ -556,16 +559,16 @@ struct ContentView: View {
if ds.contacts.mutelist == nil {
confirm_overwrite_mutelist = true
} else {
guard let keypair = ds.keypair.to_full() else {
return
}
guard let pubkey = muting else {
guard let keypair = ds.keypair.to_full(),
let pubkey = muting
else {
return
}
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: pubkey) else {
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: .pubkey(pubkey)) else {
return
}
damus_state?.contacts.set_mutelist(ev)
ds.postbox.send(ev)
}
@ -871,7 +874,7 @@ func timeline_name(_ timeline: Timeline?) -> String {
}
@discardableResult
func handle_unfollow(state: DamusState, unfollow: ReferencedId) -> Bool {
func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool {
guard let keypair = state.keypair.to_full() else {
return false
}
@ -887,27 +890,20 @@ func handle_unfollow(state: DamusState, unfollow: ReferencedId) -> Bool {
state.contacts.event = ev
if unfollow.key == "p" {
state.contacts.remove_friend(unfollow.ref_id)
switch unfollow {
case .pubkey(let pk):
state.contacts.remove_friend(pk)
state.user_search_cache.updateOwnContactsPetnames(id: state.pubkey, oldEvent: old_contacts, newEvent: ev)
case .hashtag:
// nothing to handle here really
break
}
return true
}
func handle_unfollow_notif(state: DamusState, target: FollowTarget) -> ReferencedId? {
let pk = target.pubkey
let ref = ReferencedId.p(pk)
if handle_unfollow(state: state, unfollow: ref) {
return ref
}
return nil
}
@discardableResult
func handle_follow(state: DamusState, follow: ReferencedId) -> Bool {
func handle_follow(state: DamusState, follow: FollowRef) -> Bool {
guard let keypair = state.keypair.to_full() else {
return false
}
@ -920,8 +916,12 @@ func handle_follow(state: DamusState, follow: ReferencedId) -> Bool {
notify(.followed(follow))
state.contacts.event = ev
if follow.key == "p" {
state.contacts.add_friend_pubkey(follow.ref_id)
switch follow {
case .pubkey(let pubkey):
state.contacts.add_friend_pubkey(pubkey)
case .hashtag:
// nothing to do
break
}
return true
@ -936,7 +936,7 @@ func handle_follow_notif(state: DamusState, target: FollowTarget) -> Bool {
state.contacts.add_friend_contact(ev)
}
return handle_follow(state: state, follow: .p(target.pubkey))
return handle_follow(state: state, follow: target.follow_ref)
}
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, post: NostrPostResult) -> Bool {
@ -951,7 +951,7 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
postbox.send(new_ev)
for eref in new_ev.referenced_ids.prefix(3) {
// also broadcast at most 3 referenced events
if let ev = events.lookup(eref.ref_id) {
if let ev = events.lookup(eref) {
postbox.send(ev)
}
}
@ -984,13 +984,19 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
switch link {
case .ref(let ref):
if ref.key == "p" {
result(.profile(ref.ref_id))
} else if ref.key == "e" {
find_event(state: state, query: .event(evid: ref.ref_id)) { res in
switch ref {
case .pubkey(let pk):
result(.profile(pk))
case .event(let noteid):
find_event(state: state, query: .event(evid: noteid)) { res in
guard let res, case .event(let ev) = res else { return }
result(.event(ev))
}
case .hashtag(let ht):
result(.filter(.filter_hashtag([ht.string()])))
case .param, .quote:
// doesn't really make sense here
break
}
case .filter(let filt):
result(.filter(filt))

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
}

116
damus/Nostr/Id.swift Normal file
View File

@ -0,0 +1,116 @@
//
// Id.swift
// damus
//
// Created by William Casarin on 2023-07-26.
//
import Foundation
struct TagRef<T>: Hashable, Equatable, Encodable {
let elem: TagElem
init(_ elem: TagElem) {
self.elem = elem
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(elem.string())
}
}
protocol TagKey {
var keychar: AsciiCharacter { get }
}
protocol TagKeys {
associatedtype TagKeys: TagKey
var key: TagKeys { get }
}
protocol TagConvertible {
var tag: [String] { get }
static func from_tag(tag: TagSequence) -> Self?
}
struct QuoteId: IdType, TagKey {
let id: Data
init(_ data: Data) {
self.id = data
}
var keychar: AsciiCharacter { "q" }
}
struct Privkey: IdType {
let id: Data
var nsec: String {
bech32_privkey(self)
}
init?(hex: String) {
guard let id = hex_decode_id(hex) else {
return nil
}
self.init(id)
}
init(_ data: Data) {
self.id = data
}
}
struct Hashtag: TagConvertible {
let hashtag: String
static func from_tag(tag: TagSequence) -> Hashtag? {
var i = tag.makeIterator()
guard tag.count >= 2,
let t0 = i.next(),
let chr = t0.single_char,
chr == "t",
let t1 = i.next() else {
return nil
}
return Hashtag(hashtag: t1.string())
}
var tag: [String] { ["t", self.hashtag] }
var keychar: AsciiCharacter { "t" }
}
struct ReplaceableParam: TagConvertible {
let param: TagElem
static func from_tag(tag: TagSequence) -> ReplaceableParam? {
var i = tag.makeIterator()
guard tag.count >= 2,
let t0 = i.next(),
let chr = t0.single_char,
chr == "d",
let t1 = i.next() else {
return nil
}
return ReplaceableParam(param: t1)
}
var tag: [String] { [self.keychar.description, self.param.string()] }
var keychar: AsciiCharacter { "d" }
}
struct Signature: Hashable, Equatable {
let data: Data
init(_ p: Data) {
self.data = p
}
}

View File

@ -20,16 +20,20 @@ enum ValidationResult: Decodable {
case bad_sig
}
//typealias NostrEvent = NdbNote
//typealias Tags = TagsSequence
typealias Tags = [[String]]
typealias NostrEvent = NostrEventOld
typealias NostrEvent = NdbNote
typealias TagElem = NdbTagElem
typealias Tag = TagSequence
typealias Tags = TagsSequence
//typealias TagElem = String
//typealias Tag = [TagElem]
//typealias Tags = [Tag]
//typealias NostrEvent = NostrEventOld
let MAX_NOTE_SIZE: Int = 2 << 18
/*
class NostrEventOld: Codable, Identifiable, CustomStringConvertible, Equatable, Hashable, Comparable {
// TODO: memory mapped db events
/*
private var note_data: UnsafeMutablePointer<ndb_note>
init(data: UnsafeMutablePointer<ndb_note>) {
@ -51,7 +55,6 @@ class NostrEventOld: Codable, Identifiable, CustomStringConvertible, Equatable,
}
var tags: TagIterator
*/
let id: String
let content: String
@ -90,12 +93,16 @@ class NostrEventOld: Codable, Identifiable, CustomStringConvertible, Equatable,
hasher.combine(id)
}
static func owned_from_json(json: String) -> NostrEvent? {
private enum CodingKeys: String, CodingKey {
case id, sig, tags, pubkey, created_at, kind, content
}
static func owned_from_json(json: String) -> NostrEventOld? {
let decoder = JSONDecoder()
guard let dat = json.data(using: .utf8) else {
return nil
}
guard let ev = try? decoder.decode(NostrEvent.self, from: dat) else {
guard let ev = try? decoder.decode(NostrEventOld.self, from: dat) else {
return nil
}
@ -214,10 +221,6 @@ extension NostrEventOld {
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)
}
@ -309,6 +312,7 @@ extension NostrEventOld {
return Date.now.timeIntervalSince(event_date)
}
}
*/
func sign_id(privkey: String, id: String) -> String {
let priv_key_bytes = try! privkey.bytes
@ -316,7 +320,7 @@ func sign_id(privkey: String, id: String) -> String {
// Extra params for custom signing
var aux_rand = random_bytes(count: 64)
var aux_rand = random_bytes(count: 64).bytes
var digest = try! id.bytes
// API allows for signing variable length messages
@ -326,7 +330,7 @@ func sign_id(privkey: String, id: String) -> String {
}
func decode_nostr_event(txt: String) -> NostrResponse? {
return decode_data(Data(txt.utf8))
return NostrResponse.owned_from_json(json: txt)
}
func encode_json<T: Encodable>(_ val: T) -> String? {
@ -336,7 +340,7 @@ func encode_json<T: Encodable>(_ val: T) -> String? {
}
func decode_nostr_event_json(json: String) -> NostrEvent? {
return decode_json(json)
return NostrEvent.owned_from_json(json: json)
}
/*
@ -390,7 +394,7 @@ func event_commitment(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[
let tags_data = try! tags_encoder.encode(tags)
let tags = String(decoding: tags_data, as: UTF8.self)
return "[0,\"\(pubkey)\",\(created_at),\(kind),\(tags),\(content)]"
return "[0,\"\(pubkey.hex())\",\(created_at),\(kind),\(tags),\(content)]"
}
func calculate_event_commitment(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> Data {
@ -398,9 +402,9 @@ func calculate_event_commitment(pubkey: Pubkey, created_at: UInt32, kind: UInt32
return target.data(using: .utf8)!
}
func calculate_event_id(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> Data {
func calculate_event_id(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> NoteId {
let commitment = calculate_event_commitment(pubkey: pubkey, created_at: created_at, kind: kind, tags: tags, content: content)
return sha256(commitment)
return NoteId(sha256(commitment))
}
@ -436,8 +440,6 @@ func hex_encode(_ data: Data) -> String {
return str
}
func random_bytes(count: Int) -> Data {
var bytes = [Int8](repeating: 0, count: count)
guard
@ -448,42 +450,6 @@ func random_bytes(count: Int) -> 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? {
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
let rw_relay_info = RelayInfo(read: true, write: true)
@ -511,18 +477,33 @@ func make_metadata_event(keypair: FullKeypair, metadata: Profile) -> NostrEvent?
}
func make_boost_event(keypair: FullKeypair, 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"])
var tags = boosted.tags.reduce(into: [[String]]()) { ts, tag in
guard tag.count >= 2 && (tag[0].matches_char("e") || tag[0].matches_char("p")) else {
return
}
ts.append(tag.strings())
}
tags.append(["e", boosted.id.hex(), "", "root"])
tags.append(["p", boosted.pubkey.hex()])
return NostrEvent(content: event_to_json(ev: boosted), keypair: keypair.to_keypair(), kind: 6, tags: tags)
let content = boosted.content_len <= 100 ? event_to_json(ev: boosted) : ""
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 6, tags: tags)
}
func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = "🤙") -> NostrEvent? {
var tags: [[String]] = liked.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
var tags = liked.tags.reduce(into: [[String]]()) { ts, tag in
guard tag.count >= 2,
(tag[0].matches_char("e") || tag[0].matches_char("p")) else {
return
}
ts.append(tag.strings())
}
tags.append(["e", liked.id.hex()])
tags.append(["p", liked.pubkey.hex()])
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 7, tags: tags)
}
@ -556,17 +537,19 @@ func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair,
}
func decrypt_private_zap(our_privkey: Privkey, zapreq: NostrEvent, target: ZapTarget) -> NostrEvent? {
guard let anon_tag = zapreq.tags.first(where: { t in t.count >= 2 && t[0] == "anon" }) else {
guard let anon_tag = zapreq.tags.first(where: { t in
t.count >= 2 && t[0].matches_str("anon")
}) else {
return nil
}
let enc_note = anon_tag[1]
let enc_note = anon_tag[1].string()
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 {
guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: NoteId(target.id), created_at: zapreq.created_at) else {
return nil
}
// use our private keypair and their pubkey to get the shared secret
@ -602,17 +585,15 @@ func decrypt_private_zap(our_privkey: Privkey, zapreq: NostrEvent, target: ZapTa
return note
}
func generate_private_keypair(our_privkey: String, id: String, created_at: UInt32) -> FullKeypair? {
let to_hash = our_privkey + id + String(created_at)
func generate_private_keypair(our_privkey: Privkey, id: NoteId, created_at: UInt32) -> FullKeypair? {
let to_hash = our_privkey.hex() + id.hex() + 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
}
let privkey = Privkey(privkey_bytes)
guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil }
return FullKeypair(pubkey: pubkey, privkey: privkey)
}
@ -661,7 +642,7 @@ func make_zap_request_event(keypair: FullKeypair, content: String, relays: [Rela
tags.append(["anon"])
kp = generate_new_keypair()
case .priv:
guard let priv_kp = generate_private_keypair(our_privkey: keypair.privkey, id: target.id, created_at: now) else {
guard let priv_kp = generate_private_keypair(our_privkey: keypair.privkey, id: NoteId(target.id), created_at: now) else {
return nil
}
kp = priv_kp
@ -699,27 +680,36 @@ func uniq<T: Hashable>(_ xs: [T]) -> [T] {
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] } ?? []
func gather_reply_ids(our_pubkey: Pubkey, from: NostrEvent) -> [RefId] {
var ids: [RefId] = from.referenced_ids.first.map({ ref in [ .event(ref) ] }) ?? []
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"))
let pks = from.referenced_pubkeys.reduce(into: [RefId]()) { rs, pk in
if pk == our_pubkey {
return
}
rs.append(.pubkey(pk))
}
ids.append(.event(from.id))
ids.append(contentsOf: uniq(pks))
if from.pubkey != our_pubkey {
ids.append(.pubkey(from.pubkey))
}
return ids
}
func gather_quote_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids: [ReferencedId] = [.q(from.id)]
func gather_quote_ids(our_pubkey: Pubkey, from: NostrEvent) -> [RefId] {
var ids: [RefId] = [.quote(from.id.quote_id)]
if from.pubkey != our_pubkey {
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
ids.append(.pubkey(from.pubkey))
}
return ids
}
func event_from_json(dat: String) -> NostrEvent? {
return try? JSONDecoder().decode(NostrEvent.self, from: Data(dat.utf8))
return NostrEvent.owned_from_json(json: dat)
}
func event_to_json(ev: NostrEvent) -> String {
@ -757,13 +747,10 @@ func decrypt_note(our_privkey: Privkey, their_pubkey: Pubkey, enc_note: String,
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
}
func get_shared_secret(privkey: Privkey, pubkey: Pubkey) -> [UInt8]? {
let privkey_bytes = privkey.bytes
var pk_bytes = pubkey.bytes
pk_bytes.insert(2, at: 0)
var publicKey = secp256k1_pubkey()
@ -924,45 +911,33 @@ func aes_operation(operation: CCOperation, data: [UInt8], iv: [UInt8], shared_se
func validate_event(ev: NostrEvent) -> ValidationResult {
let raw_id = calculate_event_id(pubkey: ev.pubkey, created_at: ev.created_at, kind: ev.kind, tags: ev.tags, content: ev.content)
let id = hex_encode(raw_id)
let id = calculate_event_id(pubkey: ev.pubkey, created_at: ev.created_at, kind: ev.kind, tags: ev.tags.strings(), content: ev.content)
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 ev_pubkey = ev.pubkey.id.bytes
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
var sig = ev.sig.data.bytes
var idbytes = id.id.bytes
ok = secp256k1_schnorrsig_verify(ctx, &sig, &idbytes, 32, &xonly_pubkey) > 0
return ok ? .ok : .bad_sig
}
func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
func first_eref_mention(ev: NostrEvent, privkey: Privkey?) -> Mention<NoteId>? {
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" {
guard case .mention(let mention) = block,
case .note = mention.ref else {
return false
}
@ -970,10 +945,13 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
}
/// MARK: - Preview
if let firstBlock = blocks.first, case .mention(let mention) = firstBlock, mention.ref.key == "e" {
return mention
if let firstBlock = blocks.first,
case .mention(let mention) = firstBlock,
case .note(let note_id) = mention.ref
{
return .note(note_id)
}
return nil
}
@ -999,20 +977,3 @@ func to_reaction_emoji(ev: NostrEvent) -> String? {
}
}
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"
}
}
}
}

View File

@ -16,8 +16,6 @@ enum NostrKind: UInt32, Codable {
case delete = 5
case boost = 6
case like = 7
case channel_create = 40
case channel_meta = 41
case chat = 42
case list = 30000
case longform = 30023

View File

@ -9,18 +9,18 @@ import Foundation
enum NostrLink: Equatable {
case ref(ReferencedId)
case ref(RefId)
case filter(NostrFilter)
case script([UInt8])
}
func encode_pubkey_uri(_ ref: ReferencedId) -> String {
return "p:" + ref.ref_id
func encode_pubkey_uri(_ pubkey: Pubkey) -> String {
return "p:" + pubkey.hex()
}
// TODO: bech32 and relay hints
func encode_event_id_uri(_ ref: ReferencedId) -> String {
return "e:" + ref.ref_id
func encode_event_id_uri(_ noteid: NoteId) -> String {
return "e:" + noteid.hex()
}
func parse_nostr_ref_uri_type(_ p: Parser) -> String? {
@ -55,36 +55,21 @@ func parse_hexstr(_ p: Parser, len: Int) -> String? {
return String(substring(p.str, start: start, end: p.pos))
}
func parse_nostr_ref_uri(_ p: Parser) -> ReferencedId? {
let start = p.pos
if !parse_str(p, "nostr:") {
return nil
}
guard let ref = parse_post_bech32_mention(p) else {
p.pos = start
return nil
}
return ref
}
func decode_universal_link(_ s: String) -> NostrLink? {
var uri = s.replacingOccurrences(of: "https://damus.io/r/", with: "")
uri = uri.replacingOccurrences(of: "https://damus.io/", with: "")
uri = uri.replacingOccurrences(of: "/", with: "")
guard let decoded = try? bech32_decode(uri) else {
guard let decoded = try? bech32_decode(uri),
decoded.data.count == 32
else {
return nil
}
let h = hex_encode(decoded.data)
if decoded.hrp == "note" {
return .ref(ReferencedId(ref_id: h, relay_id: nil, key: "e"))
return .ref(.event(NoteId(decoded.data)))
} else if decoded.hrp == "npub" {
return .ref(ReferencedId(ref_id: h, relay_id: nil, key: "p"))
return .ref(.pubkey(Pubkey(decoded.data)))
}
// TODO: handle nprofile, etc
@ -98,14 +83,12 @@ func decode_nostr_bech32_uri(_ s: String) -> NostrLink? {
switch obj {
case .nsec(let privkey):
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
return nil
}
return .ref(ReferencedId(ref_id: pubkey, relay_id: nil, key: "p"))
guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil }
return .ref(.pubkey(pubkey))
case .npub(let pubkey):
return .ref(ReferencedId(ref_id: pubkey, relay_id: nil, key: "p"))
return .ref(.pubkey(pubkey))
case .note(let id):
return .ref(ReferencedId(ref_id: id, relay_id: nil, key: "e"))
return .ref(.event(id))
case .nscript(let data):
return .script(data)
}
@ -134,19 +117,15 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
acc.append(decoded)
return
}
if tag_is_hashtag(parts) {
if parts.count >= 2 && parts[0] == "t" {
return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
}
if let rid = tag_to_refid(parts) {
return .ref(rid)
}
guard parts.count == 1 else {
return nil
}
let part = parts[0]
return decode_nostr_bech32_uri(part)

View File

@ -13,7 +13,12 @@ struct CommandResult {
let msg: String
}
enum NostrResponse: Decodable {
enum MaybeResponse {
case bad
case ok(NostrResponse)
}
enum NostrResponse {
case event(String, NostrEvent)
case notice(String)
case eose(String)
@ -32,48 +37,73 @@ enum NostrResponse: Decodable {
}
}
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
static func owned_from_json(json: String) -> NostrResponse? {
return json.withCString{ cstr in
let bufsize: Int = max(Int(Double(json.utf8.count) * 2.0), Int(getpagesize()))
let data = malloc(bufsize)
// Only use first item
let typ = try container.decode(String.self)
if typ == "EVENT" {
let sub_id = try container.decode(String.self)
var ev: NostrEvent
do {
ev = try container.decode(NostrEvent.self)
} catch {
print(error)
throw error
if data == nil {
let r: NostrResponse? = nil
return r
}
//ev.pow = count_hash_leading_zero_bits(ev.id)
self = .event(sub_id, ev)
return
} else if typ == "NOTICE" {
let msg = try container.decode(String.self)
self = .notice(msg)
return
} else if typ == "EOSE" {
let sub_id = try container.decode(String.self)
self = .eose(sub_id)
return
} else if typ == "OK" {
var cr: CommandResult
do {
let event_id = try container.decode(String.self)
let ok = try container.decode(Bool.self)
let msg = try container.decode(String.self)
cr = CommandResult(event_id: event_id, ok: ok, msg: msg)
} catch {
print(error)
throw error
//guard var json_cstr = json.cString(using: .utf8) else { return nil }
//json_cs
var tce = ndb_tce()
let len = ndb_ws_event_from_json(cstr, Int32(json.utf8.count), &tce, data, Int32(bufsize))
if len <= 0 {
free(data)
return nil
}
switch tce.evtype {
case NDB_TCE_OK:
defer { free(data) }
guard let evid_str = sized_cstr(cstr: tce.subid, len: tce.subid_len),
let evid = hex_decode_noteid(evid_str),
let msg = sized_cstr(cstr: tce.command_result.msg, len: tce.command_result.msglen) else {
return nil
}
let cr = CommandResult(event_id: evid, ok: tce.command_result.ok == 1, msg: msg)
return .ok(cr)
case NDB_TCE_EOSE:
defer { free(data) }
guard let subid = sized_cstr(cstr: tce.subid, len: tce.subid_len) else {
return nil
}
return .eose(subid)
case NDB_TCE_EVENT:
// Create new Data with just the valid bytes
guard let note_data = realloc(data, Int(len)) else {
free(data)
return nil
}
let new_note = note_data.assumingMemoryBound(to: ndb_note.self)
let note = NdbNote(note: new_note, owned_size: Int(len))
guard let subid = sized_cstr(cstr: tce.subid, len: tce.subid_len) else {
free(data)
return nil
}
return .event(subid, note)
case NDB_TCE_NOTICE:
free(data)
return .notice("")
default:
free(data)
return nil
}
self = .ok(cr)
return
//ev.pow = count_hash_leading_zero_bits(ev.id)
}
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "expected EVENT, NOTICE or OK, got \(typ)"))
}
}
func sized_cstr(cstr: UnsafePointer<CChar>, len: Int32) -> String? {
let msgbuf = Data(bytes: cstr, count: Int(len))
return String(data: msgbuf, encoding: .utf8)
}

View File

@ -55,3 +55,22 @@ func hex_decode(_ str: String) -> [UInt8]?
}
func hex_decode_id(_ str: String) -> Data? {
guard str.utf8.count == 64, let decoded = hex_decode(str) else {
return nil
}
return Data(decoded)
}
func hex_decode_noteid(_ str: String) -> NoteId? {
return hex_decode_id(str).map(NoteId.init)
}
func hex_decode_pubkey(_ str: String) -> Pubkey? {
return hex_decode_id(str).map(Pubkey.init)
}
func hex_decode_privkey(_ str: String) -> Privkey? {
return hex_decode_id(str).map(Privkey.init)
}

View File

@ -1,29 +0,0 @@
//
// Pubkey.swift
// damus
//
// Created by William Casarin on 2023-07-30.
//
import Foundation
// prepare a more gradual transition to the ndb branch
typealias FollowRef = ReferencedId
typealias Pubkey = String
typealias NoteId = String
typealias Privkey = String
extension String {
// Id constructors
init?(hex: String) {
self = hex
}
static var empty: String {
return ""
}
func hex() -> String {
return self
}
}

View File

@ -7,79 +7,44 @@
import Foundation
struct Reference {
let key: AsciiCharacter
let id: NdbTagElem
var ref_id: NdbTagElem {
id
}
func to_referenced_id() -> ReferencedId {
ReferencedId(ref_id: id.string(), relay_id: nil, key: key.description)
}
}
func tagref_should_be_id(_ tag: NdbTagElem) -> Bool {
return !tag.matches_char("t")
return !(tag.matches_char("t") || tag.matches_char("d"))
}
struct References: Sequence, IteratorProtocol {
struct References<T: TagConvertible>: Sequence, IteratorProtocol {
let tags: TagsSequence
var tags_iter: TagsIterator
mutating func next() -> Reference? {
while let tag = tags_iter.next() {
guard tag.count >= 2 else { continue }
let key = tag[0]
let id = tag[1]
guard key.count == 1, tagref_should_be_id(id) else { continue }
for c in key {
guard let a = AsciiCharacter(c) else { break }
return Reference(key: a, id: id)
}
}
return nil
}
static func ids(tags: TagsSequence) -> LazyFilterSequence<References> {
References(tags: tags).lazy
.filter() { ref in ref.key == "e" }
}
static func pubkeys(tags: TagsSequence) -> LazyFilterSequence<References> {
References(tags: tags).lazy
.filter() { ref in ref.key == "p" }
}
static func hashtags(tags: TagsSequence) -> LazyFilterSequence<References> {
References(tags: tags).lazy
.filter() { ref in ref.key == "t" }
}
init(tags: TagsSequence) {
self.tags = tags
self.tags_iter = tags.makeIterator()
}
}
// TagsSequence transition helpers
extension [[String]] {
func strings() -> [[String]] {
return self
mutating func next() -> T? {
while let tag = tags_iter.next() {
guard let evref = T.from_tag(tag: tag) else { continue }
return evref
}
return nil
}
}
// TagsSequence transition helpers
extension [String] {
func strings() -> [String] {
return self
extension References {
var first: T? {
self.first(where: { _ in true })
}
var last: T? {
var last: T? = nil
for t in self {
last = t
}
return last
}
}
// NdbTagElem transition helpers
extension String {
func string() -> String {
@ -99,29 +64,117 @@ extension String {
}
}
struct ReferencedId: Identifiable, Hashable, Equatable {
let ref_id: String
let relay_id: String?
let key: String
enum FollowRef: TagKeys, Hashable, TagConvertible, Equatable {
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")
// NOTE: When adding cases make sure to update key and from_tag
case pubkey(Pubkey)
case hashtag(String)
var key: FollowKeys {
switch self {
case .hashtag: return .t
case .pubkey: return .p
}
}
static func p(_ pk: String, relay_id: String? = nil) -> ReferencedId {
return ReferencedId(ref_id: pk, relay_id: relay_id, key: "p")
enum FollowKeys: AsciiCharacter, TagKey, CustomStringConvertible {
case p, t
var keychar: AsciiCharacter { self.rawValue }
var description: String { self.rawValue.description }
}
static func t(_ hashtag: String, relay_id: String? = nil) -> ReferencedId {
return ReferencedId(ref_id: hashtag, relay_id: relay_id, key: "t")
static func from_tag(tag: TagSequence) -> FollowRef? {
guard tag.count >= 2 else { return nil }
var i = tag.makeIterator()
guard let t0 = i.next(),
let c = t0.single_char,
let fkey = FollowKeys(rawValue: c),
let t1 = i.next()
else {
return nil
}
switch fkey {
case .p: return t1.id().map({ .pubkey(Pubkey($0)) })
case .t: return .hashtag(t1.string())
}
}
var tag: [String] {
[key.description, self.description]
}
var description: String {
switch self {
case .pubkey(let pubkey): return pubkey.description
case .hashtag(let string): return string
}
}
}
enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case event(NoteId)
case pubkey(Pubkey)
case quote(QuoteId)
case hashtag(TagElem)
case param(TagElem)
var key: RefKey {
switch self {
case .event: return .e
case .pubkey: return .p
case .quote: return .q
case .hashtag: return .t
case .param: return .d
}
}
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
case e, p, t, d, q
var keychar: AsciiCharacter {
self.rawValue
}
var description: String {
self.keychar.description
}
}
var tag: [String] {
[self.key.description, self.description]
}
var description: String {
switch self {
case .event(let noteId): return noteId.hex()
case .pubkey(let pubkey): return pubkey.hex()
case .quote(let quote): return quote.hex()
case .hashtag(let string): return string.string()
case .param(let string): return string.string()
}
}
static func from_tag(tag: TagSequence) -> RefId? {
var i = tag.makeIterator()
guard tag.count >= 2,
let t0 = i.next(),
let key = t0.single_char,
let rkey = RefKey(rawValue: key),
let t1 = i.next()
else { return nil }
switch rkey {
case .e: return t1.id().map({ .event(NoteId($0)) })
case .p: return t1.id().map({ .pubkey(Pubkey($0)) })
case .q: return t1.id().map({ .quote(QuoteId($0)) })
case .t: return .hashtag(t1)
case .d: return .param(t1)
}
}
}

View File

@ -200,6 +200,7 @@ final class RelayConnection: ObservableObject {
}
return
}
print("failed to decode event \(messageString)")
case .data(let messageData):
if let messageString = String(data: messageData, encoding: .utf8) {
receive(message: .string(messageString))

View File

@ -7,8 +7,14 @@
import Foundation
let test_seckey = "8e33316b227de8215d36f4787573beaaf532229bb00398430a0ae963b658e656"
let test_pubkey = "a9952fe066ced622167acb8069a0dfd1d44d9493ef2a4c28cf93e2877248b41a"
let test_seckey = Privkey(Data([0xe0, 0xaa, 0x60, 0x26, 0x08, 0x18, 0xac, 0x10, 0x03, 0x86, 0x4d, 0x15, 0x24, 0x9a, 0xf7, 0xa3, 0x3e, 0x4f, 0x1f, 0xc9, 0x01, 0xcf, 0xee, 0xa9, 0xb4, 0x77, 0xc7, 0x07, 0x22, 0xb7, 0x25, 0xfd]))
let test_pubkey = Pubkey(Data([0xf7, 0xda, 0xc4, 0x6a, 0xa2, 0x70, 0xf7, 0x28, 0x76, 0x06, 0xa2, 0x2b, 0xeb, 0x4d, 0x77, 0x25, 0x57, 0x3a, 0xfa, 0x0e, 0x02, 0x8c, 0xdf, 0xac, 0x39, 0xa4, 0xcb, 0x23, 0x31, 0x53, 0x7f, 0x66]))
let test_pubkey_2 = Pubkey(Data([0x18, 0x42, 0x95, 0xc7, 0x6d, 0x5f, 0xf9, 0x4e, 0x99, 0x6a, 0xa8, 0xc1, 0x75, 0x23, 0x93, 0xdf, 0x0e, 0x72, 0xb5, 0x51, 0x89, 0xfc, 0x88, 0xfa, 0x06, 0x41, 0x5c, 0xce, 0x20, 0x4a, 0xc5, 0xea]))
let test_keypair = Keypair(pubkey: test_pubkey, privkey: test_seckey)
let test_keypair_full = test_keypair.to_full()!

View File

@ -0,0 +1,62 @@
//
// IdType.swift
// damus
//
// Created by William Casarin on 2023-07-28.
//
import Foundation
protocol IdType: Codable, CustomStringConvertible, Hashable, Equatable {
var id: Data { get }
init(_ data: Data)
init(from decoder: Decoder) throws
func encode(to encoder: Encoder) throws
}
extension IdType {
func hex() -> String {
hex_encode(self.id)
}
var bytes: [UInt8] {
self.id.bytes
}
static var empty: Self {
return Self.init(Data(repeating: 0, count: 32))
}
var description: String {
self.hex()
}
init(from decoder: Decoder) throws {
self.init(try hex_decoder(decoder))
}
func encode(to encoder: Encoder) throws {
try hex_encoder(to: encoder, data: self.id)
}
}
func hex_decoder(_ decoder: Decoder, expected_len: Int = 32) throws -> Data {
let container = try decoder.singleValueContainer()
guard let arr = hex_decode(try container.decode(String.self)) else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "hex string"))
}
if arr.count != expected_len {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "too long"))
}
return Data(bytes: arr, count: arr.count)
}
func hex_encoder(to encoder: Encoder, data: Data) throws {
var container = encoder.singleValueContainer()
try container.encode(hex_encode(data))
}

View File

@ -0,0 +1,52 @@
//
// NoteId.swift
// damus
//
// Created by William Casarin on 2023-07-28.
//
import Foundation
struct NoteId: IdType, TagKey, TagConvertible {
let id: Data
init(_ data: Data) {
self.id = data
}
init?(hex: String) {
guard let note_id = hex_decode_noteid(hex) else {
return nil
}
self = note_id
}
var bech32: String {
bech32_note_id(self)
}
/// Refer to this NoteId as a QuoteId
var quote_id: QuoteId {
QuoteId(self.id)
}
var keychar: AsciiCharacter { "e" }
var tag: [String] {
["e", self.hex()]
}
static func from_tag(tag: TagSequence) -> NoteId? {
var i = tag.makeIterator()
guard tag.count >= 2,
let t0 = i.next(),
let key = t0.single_char,
key == "e",
let t1 = i.next(),
let note_id = t1.id().map(NoteId.init)
else { return nil }
return note_id
}
}

View File

@ -0,0 +1,48 @@
//
// Pubkey.swift
// damus
//
// Created by William Casarin on 2023-07-28.
//
import Foundation
struct Pubkey: IdType, TagKey, TagConvertible, Identifiable {
let id: Data
var tag: [String] {
[keychar.description, self.hex()]
}
init?(hex: String) {
guard let id = hex_decode_pubkey(hex) else {
return nil
}
self = id
}
init(_ data: Data) {
self.id = data
}
var npub: String {
bech32_pubkey(self)
}
var keychar: AsciiCharacter { "p" }
static func from_tag(tag: TagSequence) -> Pubkey? {
var i = tag.makeIterator()
guard tag.count >= 2,
let t0 = i.next(),
let key = t0.single_char,
key == "p",
let t1 = i.next(),
let pubkey = t1.id().map(Pubkey.init)
else { return nil }
return pubkey
}
}

View File

@ -0,0 +1,91 @@
//
// Referenced.swift
// damus
//
// Created by William Casarin on 2023-07-28.
//
import Foundation
enum Marker: String {
case root
case reply
case mention
init?(_ tag: TagElem) {
let len = tag.count
if len == 4, tag.matches_str("root", tag_len: len) {
self = .root
} else if len == 5, tag.matches_str("reply", tag_len: len) {
self = .reply
} else if len == 7, tag.matches_str("mention", tag_len: len) {
self = .mention
} else {
return nil
}
}
}
struct NoteRef: IdType, TagConvertible, Equatable {
let note_id: NoteId
let relay: String?
let marker: Marker?
var id: Data {
self.note_id.id
}
init(note_id: NoteId, relay: String? = nil, marker: Marker? = nil) {
self.note_id = note_id
self.relay = relay
self.marker = marker
}
static func note_id(_ note_id: NoteId) -> NoteRef {
return NoteRef(note_id: note_id)
}
init(_ data: Data) {
self.note_id = NoteId(data)
self.relay = nil
self.marker = nil
}
var tag: [String] {
var t = ["e", self.hex()]
if let marker {
t.append(relay ?? "")
t.append(marker.rawValue)
} else if let relay {
t.append(relay)
}
return t
}
static func from_tag(tag: TagSequence) -> NoteRef? {
guard tag.count >= 2 else { return nil }
var i = tag.makeIterator()
guard let t0 = i.next(),
t0.single_char == "e",
let t1 = i.next(),
let note_id = t1.id().map(NoteId.init)
else {
return nil
}
var relay: String? = nil
var marker: Marker? = nil
if tag.count >= 3, let r = i.next() {
relay = r.string()
if tag.count >= 4, let m = i.next() {
marker = Marker(m)
}
}
return NoteRef(note_id: note_id, relay: relay, marker: marker)
}
}

View File

@ -20,11 +20,11 @@ enum Bech32Object {
}
if decoded.hrp == "npub" {
return .npub(hex_encode(decoded.data))
return .npub(Pubkey(decoded.data))
} else if decoded.hrp == "nsec" {
return .nsec(hex_encode(decoded.data))
return .nsec(Privkey(decoded.data))
} else if decoded.hrp == "note" {
return .note(hex_encode(decoded.data))
return .note(NoteId(decoded.data))
} else if decoded.hrp == "nscript" {
return .nscript(decoded.data.bytes)
}

View File

@ -17,8 +17,11 @@ final class CredentialHandler: NSObject, ASAuthorizationControllerDelegate {
authorizationController.performRequests()
}
func save_credential(pubkey: String, privkey: String) {
SecAddSharedWebCredential("damus.io" as CFString, pubkey as CFString, privkey as CFString, { error in
func save_credential(pubkey: Pubkey, privkey: Privkey) {
let pub = pubkey.npub
let priv = privkey.nsec
SecAddSharedWebCredential("damus.io" as CFString, pub as CFString, priv as CFString, { error in
if let error {
print("⚠️ An error occurred while saving credentials: \(error)")
}

View File

@ -60,7 +60,6 @@ func parse_display_name(profile: Profile?, pubkey: Pubkey) -> DisplayName {
return .one(abbrev_bech32_pubkey(pubkey: pubkey))
}
func abbrev_bech32_pubkey(pubkey: String) -> String {
let pk = bech32_nopre_pubkey(pubkey) ?? pubkey
return abbrev_pubkey(pk)
func abbrev_bech32_pubkey(pubkey: Pubkey) -> String {
return abbrev_pubkey(String(pubkey.npub.dropFirst(4)))
}

View File

@ -87,12 +87,12 @@ class ZapsDataModel: ObservableObject {
}
@discardableResult
func remove(reqid: String) -> Bool {
guard zaps.first(where: { z in z.request.ev.id == reqid }) != nil else {
func remove(reqid: ZapRequestId) -> Bool {
guard zaps.first(where: { z in z.request.id == reqid }) != nil else {
return false
}
self.zaps = zaps.filter { z in z.request.ev.id != reqid }
self.zaps = zaps.filter { z in z.request.id != reqid }
return true
}
}
@ -140,10 +140,10 @@ class EventCache {
private var events: [NoteId: NostrEvent] = [:]
private var replies = ReplyMap()
private var cancellable: AnyCancellable?
private var image_metadata: [String: ImageMetadataState] = [:]
private var video_meta: [String: VideoPlayerModel] = [:]
private var event_data: [String: EventData] = [:]
private var image_metadata: [String: ImageMetadataState] = [:] // lowercased URL key
private var video_meta: [URL: VideoPlayerModel] = [:]
private var event_data: [NoteId: EventData] = [:]
//private var thread_latest: [String: Int64]
init() {
@ -174,7 +174,7 @@ class EventCache {
@discardableResult
func store_zap(zap: Zapping) -> Bool {
let data = get_cache_data(zap.target.id).zaps_model
let data = get_cache_data(NoteId(zap.target.id)).zaps_model
if let ev = zap.event {
insert(ev)
}
@ -185,7 +185,7 @@ class EventCache {
switch zap.target {
case .note(let note_target):
let zaps = get_cache_data(note_target.note_id).zaps_model
zaps.remove(reqid: zap.request.ev.id)
zaps.remove(reqid: zap.request.id)
case .profile:
// these aren't stored anywhere yet
break
@ -193,7 +193,7 @@ class EventCache {
}
func lookup_zaps(target: ZapTarget) -> [Zapping] {
return get_cache_data(target.id).zaps_model.zaps
return get_cache_data(NoteId(target.id)).zaps_model.zaps
}
func store_img_metadata(url: URL, meta: ImageMetadataState) {
@ -214,31 +214,29 @@ class EventCache {
}
func store_video_player_model(url: URL, meta: VideoPlayerModel) {
video_meta[url.absoluteString] = meta
video_meta[url] = meta
}
@MainActor
func get_video_player_model(url: URL) -> VideoPlayerModel {
if let model = video_meta[url.absoluteString] {
if let model = video_meta[url] {
return model
}
let model = VideoPlayerModel()
video_meta[url.absoluteString] = model
video_meta[url] = model
return model
}
func parent_events(event: NostrEvent) -> [NostrEvent] {
func parent_events(event: NostrEvent, privkey: Privkey?) -> [NostrEvent] {
var parents: [NostrEvent] = []
var ev = event
while true {
guard let direct_reply = ev.direct_replies(nil).last else {
break
}
guard let next_ev = lookup(direct_reply.ref_id), next_ev != ev else {
guard let direct_reply = ev.direct_replies(privkey).last,
let next_ev = lookup(direct_reply), next_ev != ev
else {
break
}
@ -249,9 +247,9 @@ class EventCache {
return parents.reversed()
}
func add_replies(ev: NostrEvent) {
for reply in ev.direct_replies(nil) {
replies.add(id: reply.ref_id, reply_id: ev.id)
func add_replies(ev: NostrEvent, privkey: Privkey?) {
for reply in ev.direct_replies(privkey) {
replies.add(id: reply, reply_id: ev.id)
}
}
@ -360,7 +358,7 @@ func get_preload_plan(evcache: EventCache, ev: NostrEvent, our_keypair: Keypair,
}
// Cached event might not have the note language determined yet, so determine the language here before figuring out if translations should be preloaded.
let note_lang = cache.translations_model.note_language ?? ev.note_language(our_keypair.privkey) ?? current_language()
let note_lang = cache.translations_model.note_language ?? /*ev.note_language(our_keypair.privkey)*/ current_language()
let load_translations = should_preload_translation(event: ev, our_keypair: our_keypair, current_status: cache.translations, settings: settings, note_lang: note_lang)
if load_translations {
@ -459,7 +457,7 @@ func preload_event(plan: PreloadPlan, state: DamusState) async {
}
let note_language = plan.data.translations_model.note_language ?? plan.event.note_language(our_keypair.privkey) ?? current_language()
var translations: TranslateStatus? = nil
// We have to recheck should_translate here now that we have note_language
if plan.load_translations && should_translate(event: plan.event, our_keypair: our_keypair, settings: settings, note_lang: note_language) && settings.auto_translate

View File

@ -9,7 +9,7 @@ import Foundation
/// Used for holding back events until they're ready to be displayed
class EventHolder: ObservableObject, ScrollQueue {
private var has_event = Set<String>()
private var has_event = Set<NoteId>()
@Published var events: [NostrEvent]
var incoming: [NostrEvent]
var should_queue = false

View File

@ -9,7 +9,14 @@ import Foundation
import secp256k1
let PUBKEY_HRP = "npub"
let ANON_PUBKEY = "anon"
// some random pubkey
let ANON_PUBKEY = Pubkey(Data([
0x85, 0x41, 0x5d, 0x63, 0x5c, 0x2b, 0xaf, 0x55,
0xf5, 0xb9, 0xa1, 0xa6, 0xce, 0xb7, 0x75, 0xcc,
0x5c, 0x45, 0x4a, 0x3a, 0x61, 0xb5, 0x3f, 0xe8,
0x50, 0x42, 0xdc, 0x42, 0xac, 0xe1, 0x7f, 0x12
]))
struct FullKeypair: Equatable {
let pubkey: Pubkey
@ -41,8 +48,8 @@ struct Keypair {
init(pubkey: Pubkey, privkey: Privkey?) {
self.pubkey = pubkey
self.privkey = privkey
self.pubkey_bech32 = bech32_pubkey(pubkey) ?? pubkey
self.privkey_bech32 = privkey.flatMap { bech32_privkey($0) }
self.pubkey_bech32 = pubkey.npub
self.privkey_bech32 = privkey?.nsec
}
}
@ -52,60 +59,52 @@ enum Bech32Key {
}
func decode_bech32_key(_ key: String) -> Bech32Key? {
guard let decoded = try? bech32_decode(key) else {
guard let decoded = try? bech32_decode(key),
decoded.data.count == 32
else {
return nil
}
let hexed = hex_encode(decoded.data)
if decoded.hrp == "npub" {
return .pub(hexed)
return .pub(Pubkey(decoded.data))
} else if decoded.hrp == "nsec" {
return .sec(hexed)
return .sec(Privkey(decoded.data))
}
return nil
}
func bech32_privkey(_ privkey: String) -> String? {
guard let bytes = hex_decode(privkey) else {
return nil
}
return bech32_encode(hrp: "nsec", bytes)
func bech32_privkey(_ privkey: Privkey) -> String {
return bech32_encode(hrp: "nsec", privkey.bytes)
}
func bech32_pubkey(_ pubkey: String) -> String? {
guard let bytes = hex_decode(pubkey) else {
return nil
}
return bech32_encode(hrp: "npub", bytes)
func bech32_pubkey(_ pubkey: Pubkey) -> String {
return bech32_encode(hrp: "npub", pubkey.bytes)
}
func bech32_pubkey_decode(_ pubkey: String) -> String? {
guard let decoded = try? bech32_decode(pubkey), decoded.hrp == "npub" else {
func bech32_pubkey_decode(_ pubkey: String) -> Pubkey? {
guard let decoded = try? bech32_decode(pubkey),
decoded.hrp == "npub",
decoded.data.count == 32
else {
return nil
}
return hex_encode(decoded.data)
return Pubkey(decoded.data)
}
func bech32_nopre_pubkey(_ pubkey: String) -> String? {
guard let bytes = hex_decode(pubkey) else {
return nil
}
return bech32_encode(hrp: "", bytes)
func bech32_nopre_pubkey(_ pubkey: Pubkey) -> String {
return bech32_encode(hrp: "", pubkey.bytes)
}
func bech32_note_id(_ evid: String) -> String? {
guard let bytes = hex_decode(evid) else {
return nil
}
return bech32_encode(hrp: "note", bytes)
func bech32_note_id(_ evid: NoteId) -> String {
return bech32_encode(hrp: "note", evid.bytes)
}
func generate_new_keypair() -> FullKeypair {
let key = try! secp256k1.Signing.PrivateKey()
let privkey = hex_encode(key.rawRepresentation)
let pubkey = hex_encode(Data(key.publicKey.xonly.bytes))
let privkey = Privkey(key.rawRepresentation)
let pubkey = Pubkey(Data(key.publicKey.xonly.bytes))
return FullKeypair(pubkey: pubkey, privkey: privkey)
}
@ -113,12 +112,11 @@ func privkey_to_pubkey_raw(sec: [UInt8]) -> Pubkey? {
guard let key = try? secp256k1.Signing.PrivateKey(rawRepresentation: sec) else {
return nil
}
return hex_encode(Data(key.publicKey.xonly.bytes))
return Pubkey(Data(key.publicKey.xonly.bytes))
}
func privkey_to_pubkey(privkey: String) -> String? {
guard let sec = hex_decode(privkey) else { return nil }
return privkey_to_pubkey_raw(sec: sec)
func privkey_to_pubkey(privkey: Privkey) -> Pubkey? {
return privkey_to_pubkey_raw(sec: privkey.bytes)
}
func save_pubkey(pubkey: Pubkey) {
@ -155,11 +153,18 @@ func clear_keypair() throws {
func get_saved_keypair() -> Keypair? {
do {
try removePrivateKeyFromUserDefaults()
return get_saved_pubkey().flatMap { pubkey in
let privkey = get_saved_privkey()
return Keypair(pubkey: pubkey, privkey: privkey)
guard let pubkey = get_saved_pubkey(),
let pk = hex_decode(pubkey)
else {
return nil
}
let privkey = get_saved_privkey().flatMap { sec in
hex_decode(sec).map { Privkey(Data($0)) }
}
return Keypair(pubkey: Pubkey(Data(pk)), privkey: privkey)
} catch {
return nil
}
@ -189,7 +194,10 @@ func contentContainsPrivateKey(_ content: String) -> Bool {
}
fileprivate func removePrivateKeyFromUserDefaults() throws {
guard let privKey = UserDefaults.standard.string(forKey: "privkey") else { return }
try save_privkey(privkey: privKey)
guard let privkey_str = UserDefaults.standard.string(forKey: "privkey"),
let privkey = hex_decode_privkey(privkey_str)
else { return }
try save_privkey(privkey: privkey)
UserDefaults.standard.removeObject(forKey: "privkey")
}

View File

@ -7,64 +7,62 @@
import Foundation
func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: String) -> NostrEvent? {
func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: RefId) -> NostrEvent? {
return create_or_update_list_event(keypair: keypair, mprev: mprev, to_add: to_add, list_name: "mute", list_type: "p")
}
func remove_from_mutelist(keypair: FullKeypair, prev: NostrEvent, to_remove: String) -> NostrEvent? {
return remove_from_list_event(keypair: keypair, prev: prev, to_remove: to_remove, tag_type: "p")
func remove_from_mutelist(keypair: FullKeypair, prev: NostrEvent, to_remove: RefId) -> NostrEvent? {
return remove_from_list_event(keypair: keypair, prev: prev, to_remove: to_remove)
}
func create_or_update_list_event(keypair: FullKeypair, mprev: NostrEvent?, to_add: String, list_name: String, list_type: String) -> NostrEvent? {
func create_or_update_list_event(keypair: FullKeypair, mprev: NostrEvent?, to_add: RefId, list_name: String, list_type: String) -> NostrEvent? {
if let prev = mprev,
prev.pubkey == keypair.pubkey,
matches_list_name(tags: prev.tags, name: list_name)
{
return add_to_list_event(keypair: keypair, prev: prev, to_add: to_add, tag_type: list_type)
return add_to_list_event(keypair: keypair, prev: prev, to_add: to_add)
}
let tags = [["d", list_name], [list_type, to_add]]
let tags = [["d", list_name], [list_type, to_add.description]]
return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 30000, tags: tags)
}
func remove_from_list_event(keypair: FullKeypair, prev: NostrEvent, to_remove: String, tag_type: String) -> NostrEvent? {
var exists = false
for tag in prev.tags {
if tag.count >= 2 && tag[0] == tag_type && tag[1] == to_remove {
exists = true
func remove_from_list_event(keypair: FullKeypair, prev: NostrEvent, to_remove: RefId) -> NostrEvent? {
var removed = false
let tags = prev.tags.reduce(into: [[String]](), { acc, tag in
if let ref_id = RefId.from_tag(tag: tag), ref_id == to_remove {
removed = true
return
}
}
// make sure we actually have the pubkey to remove
guard exists else {
acc.append(tag.strings())
})
guard removed else {
return nil
}
let new_tags = prev.tags.filter { tag in
!(tag.count >= 2 && tag[0] == tag_type && tag[1] == to_remove)
}
return NostrEvent(content: prev.content, keypair: keypair.to_keypair(), kind: 30000, tags: new_tags)
}
func add_to_list_event(keypair: FullKeypair, prev: NostrEvent, to_add: String, tag_type: String) -> NostrEvent? {
for tag in prev.tags {
// we are already muting this user
if tag.count >= 2 && tag[0] == tag_type && tag[1] == to_add {
return nil
}
}
var tags = Array(prev.tags)
tags.append([tag_type, to_add])
return NostrEvent(content: prev.content, keypair: keypair.to_keypair(), kind: 30000, tags: tags)
}
func matches_list_name(tags: [[String]], name: String) -> Bool {
func add_to_list_event(keypair: FullKeypair, prev: NostrEvent, to_add: RefId) -> NostrEvent? {
for tag in prev.tags {
// we are already muting this user
if let ref = RefId.from_tag(tag: tag), to_add == ref {
return nil
}
}
var tags = prev.tags.strings()
tags.append(to_add.tag)
return NostrEvent(content: prev.content, keypair: keypair.to_keypair(), kind: 30000, tags: tags)
}
func matches_list_name(tags: Tags, name: String) -> Bool {
for tag in tags {
if tag.count >= 2 && tag[0] == "d" {
return tag[1] == name
if tag.count >= 2 && tag[0].matches_char("d") {
return tag[1].matches_str(name)
}
}

View File

@ -9,21 +9,21 @@ import Foundation
struct LossyLocalNotification {
let type: LocalNotificationType
let event_id: String
let mention: MentionRef
func to_user_info() -> [AnyHashable: Any] {
return [
"type": self.type.rawValue,
"evid": self.event_id
"id": self.mention.bech32
]
}
static func from_user_info(user_info: [AnyHashable: Any]) -> LossyLocalNotification {
let target_id = user_info["evid"] as! String
let target_id = MentionRef.from_bech32(str: user_info["id"] as! String)!
let typestr = user_info["type"] as! String
let type = LocalNotificationType(rawValue: typestr)!
return LossyLocalNotification(type: type, event_id: target_id)
return LossyLocalNotification(type: type, mention: target_id)
}
}
@ -34,7 +34,7 @@ struct LocalNotification {
let content: String
func to_lossy() -> LossyLocalNotification {
return LossyLocalNotification(type: self.type, event_id: self.target.id)
return LossyLocalNotification(type: self.type, mention: .note(self.target.id))
}
}

View File

@ -28,7 +28,7 @@ class ReplyCounter {
return replies[evid] ?? 0
}
func count_replies(_ event: NostrEvent) {
func count_replies(_ event: NostrEvent, privkey: Privkey?) {
guard event.is_textlike else {
return
}
@ -39,15 +39,15 @@ class ReplyCounter {
counted.insert(event.id)
for reply in event.direct_replies(nil) {
for reply in event.direct_replies(privkey) {
if event.pubkey == our_pubkey {
self.our_replies[reply.ref_id] = event
self.our_replies[reply] = event
}
if replies[reply.ref_id] != nil {
replies[reply.ref_id] = replies[reply.ref_id]! + 1
if replies[reply] != nil {
replies[reply] = replies[reply]! + 1
} else {
replies[reply.ref_id] = 1
replies[reply] = 1
}
}
}

View File

@ -38,21 +38,25 @@ struct WalletConnectURL: Equatable {
init?(str: String) {
guard let url = URL(string: str),
url.scheme == "nostrwalletconnect" || url.scheme == "nostr+walletconnect",
let pk = url.host, pk.utf8.count == 64,
let pkhost = url.host,
let pubkey = hex_decode_pubkey(pkhost),
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let items = components.queryItems,
let relay = items.first(where: { qi in qi.name == "relay" })?.value,
let relay_url = RelayURL(relay),
let secret = items.first(where: { qi in qi.name == "secret" })?.value,
secret.utf8.count == 64,
let our_pk = privkey_to_pubkey(privkey: secret)
let decoded = hex_decode(secret)
else {
return nil
}
let privkey = Privkey(Data(decoded))
guard let our_pk = privkey_to_pubkey(privkey: privkey) else { return nil }
let lud16 = items.first(where: { qi in qi.name == "lud16" })?.value
let keypair = FullKeypair(pubkey: our_pk, privkey: secret)
self = WalletConnectURL(pubkey: pk, relay: relay_url, keypair: keypair, lud16: lud16)
let keypair = FullKeypair(pubkey: our_pk, privkey: privkey)
self = WalletConnectURL(pubkey: pubkey, relay: relay_url, keypair: keypair, lud16: lud16)
}
init(pubkey: Pubkey, relay: RelayURL, keypair: FullKeypair, lud16: String?) {
@ -90,11 +94,11 @@ struct FullWalletResponse {
let response: WalletResponse
init?(from: NostrEvent, nwc: WalletConnectURL) async {
guard let req_id = from.referenced_ids.first else {
guard let note_id = from.referenced_ids.first else {
return nil
}
self.req_id = req_id.ref_id.string()
self.req_id = note_id
let ares = Task {
guard let json = decrypt_dm(nwc.keypair.privkey, pubkey: nwc.pubkey, content: from.content, encoding: .base64),
@ -166,7 +170,7 @@ struct PayInvoiceRequest: Codable {
}
func make_wallet_connect_request<T>(req: WalletRequest<T>, to_pk: Pubkey, keypair: FullKeypair) -> NostrEvent? {
let tags = [["p", to_pk]]
let tags = [to_pk.tag]
let created_at = UInt32(Date().timeIntervalSince1970)
guard let content = encode_json(req) else {
return nil
@ -213,7 +217,7 @@ func nwc_success(state: DamusState, resp: FullWalletResponse) {
if nwc_state.update_state(state: .confirmed) {
// notify the zaps model of an update so it can mark them as paid
state.events.get_cache_data(pzap.target.id).zaps_model.objectWillChange.send()
state.events.get_cache_data(NoteId(pzap.target.id)).zaps_model.objectWillChange.send()
print("NWC success confirmed")
}

View File

@ -28,13 +28,22 @@ enum ZapTarget: Equatable, Hashable {
return note_target.author
}
}
var id: String {
var note_id: NoteId? {
switch self {
case .note(let note_target):
return note_target.note_id
case .profile(let pk):
return pk
case .profile:
return nil
case .note(let noteZapTarget):
return noteZapTarget.note_id
}
}
var id: Data {
switch self {
case .profile(let pubkey):
return pubkey.id
case .note(let noteZapTarget):
return noteZapTarget.note_id.id
}
}
}
@ -42,7 +51,11 @@ enum ZapTarget: Equatable, Hashable {
struct ZapRequest {
let ev: NostrEvent
let marked_hidden: Bool
var id: ZapRequestId {
ZapRequestId(from_zap_request: self)
}
var is_in_thread: Bool {
return !self.ev.content.isEmpty && !marked_hidden
}
@ -134,9 +147,13 @@ class PendingZap {
}
}
struct ZapRequestId: Equatable {
let reqid: String
struct ZapRequestId: Equatable, Hashable {
let reqid: NoteId
init(from_zap_request: ZapRequest) {
self.reqid = from_zap_request.ev.id
}
init(from_zap: Zapping) {
self.reqid = from_zap.request.ev.id
}
@ -348,11 +365,11 @@ func invoice_to_zap_invoice(_ invoice: Invoice) -> ZapInvoice? {
}
func determine_zap_target(_ ev: NostrEvent) -> ZapTarget? {
guard let ptag = event_tag(ev, name: "p") else {
guard let ptag = ev.referenced_pubkeys.first else {
return nil
}
if let etag = event_tag(ev, name: "e") {
if let etag = ev.referenced_ids.first {
return ZapTarget.note(id: etag, author: ptag)
}
@ -376,7 +393,7 @@ func decode_bolt11(_ s: String) -> Invoice? {
let block = bs.blocks[0]
guard let converted = convert_block(block, tags: []) else {
guard let converted = convert_block(block, tags: nil) else {
blocks_free(&bs)
return nil
}
@ -405,20 +422,16 @@ func decode_nostr_event_json(_ desc: String) -> NostrEvent? {
}
func fetch_zapper_from_lnurl(lnurls: LNUrls, pubkey: String, lnurl: String) async -> String? {
guard let endpoint = await lnurls.lookup_or_fetch(pubkey: pubkey, lnurl: lnurl) else {
func fetch_zapper_from_lnurl(lnurls: LNUrls, pubkey: Pubkey, lnurl: String) async -> Pubkey? {
guard let endpoint = await lnurls.lookup_or_fetch(pubkey: pubkey, lnurl: lnurl),
let allows = endpoint.allowsNostr, allows,
let key = endpoint.nostrPubkey,
let pk = hex_decode_pubkey(key)
else {
return nil
}
guard let allows = endpoint.allowsNostr, allows else {
return nil
}
guard let key = endpoint.nostrPubkey, key.count == 64 else {
return nil
}
return endpoint.nostrPubkey
return pk
}
func decode_lnurl(_ lnurl: String) -> URL? {

View File

@ -34,14 +34,17 @@ class Zaps {
res = zap
our_zaps[kv.key] = ours.filter { z in z.request.ev.id != reqid }
if let count = event_counts[zap.target.id] {
event_counts[zap.target.id] = count - 1
// counts for note zaps
if let note_id = zap.target.note_id {
if let count = event_counts[note_id] {
event_counts[note_id] = count - 1
}
if let total = event_totals[note_id] {
event_totals[note_id] = total - zap.amount
}
}
if let total = event_totals[zap.target.id] {
event_totals[zap.target.id] = total - zap.amount
}
// we found the request id, we can stop looking
break
}
@ -55,6 +58,7 @@ class Zaps {
return
}
self.zaps[zap.request.ev.id] = zap
if let zap_id = zap.event?.id {
self.zaps[zap_id] = zap
}
@ -62,11 +66,12 @@ class Zaps {
// record our zaps for an event
if zap.request.ev.pubkey == our_pubkey {
switch zap.target {
case .note(let note_target):
if our_zaps[note_target.note_id] == nil {
our_zaps[note_target.note_id] = [zap]
case .note(let note_zap):
let note_id = note_zap.note_id
if our_zaps[note_id] == nil {
our_zaps[note_id] = [zap]
} else {
insert_uniq_sorted_zap_by_amount(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
insert_uniq_sorted_zap_by_amount(zaps: &(our_zaps[note_id]!), new_zap: zap)
}
case .profile:
break
@ -78,19 +83,20 @@ class Zaps {
return
}
let id = zap.target.id
if event_counts[id] == nil {
event_counts[id] = 0
}
if event_totals[id] == nil {
event_totals[id] = 0
}
event_counts[id] = event_counts[id]! + 1
event_totals[id] = event_totals[id]! + zap.amount
if let note_id = zap.target.note_id {
if event_counts[note_id] == nil {
event_counts[note_id] = 0
}
notify(.update_stats(note_id: zap.target.id))
if event_totals[note_id] == nil {
event_totals[note_id] = 0
}
event_counts[note_id] = event_counts[note_id]! + 1
event_totals[note_id] = event_totals[note_id]! + zap.amount
notify(.update_stats(note_id: note_id))
}
}
}
@ -98,5 +104,5 @@ func remove_zap(reqid: ZapRequestId, zapcache: Zaps, evcache: EventCache) {
guard let zap = zapcache.remove_zap(reqid: reqid.reqid) else {
return
}
evcache.get_cache_data(zap.target.id).zaps_model.remove(reqid: reqid.reqid)
evcache.get_cache_data(NoteId(zap.target.id)).zaps_model.remove(reqid: reqid)
}

View File

@ -106,11 +106,7 @@ struct EventActionBar: View {
}
}
.sheet(isPresented: $show_share_sheet, onDismiss: { self.show_share_sheet = false }) {
if let note_id = bech32_note_id(event.id) {
if let url = URL(string: "https://damus.io/" + note_id) {
ShareSheet(activityItems: [url])
}
}
ShareSheet(activityItems: [URL(string: "https://damus.io/" + event.id.bech32)!])
}
.sheet(isPresented: $show_repost_action, onDismiss: { self.show_repost_action = false }) {

View File

@ -38,7 +38,7 @@ struct ShareAction: View {
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note")) {
dismiss()
UIPasteboard.general.string = "https://damus.io/" + (bech32_note_id(event.id) ?? event.id)
UIPasteboard.general.string = "https://damus.io/" + event.id.bech32
}
let bookmarkImg = isBookmarked ? "bookmark.fill" : "bookmark"

View File

@ -36,18 +36,18 @@ struct GradientFollowButton: View {
)
}
.onReceive(handle_notify(.followed)) { ref in
guard target.pubkey == ref.ref_id else { return }
guard target.follow_ref == ref else { return }
self.follow_state = .follows
}
.onReceive(handle_notify(.unfollowed)) { ref in
guard target.pubkey == ref.ref_id else { return }
guard target.follow_ref == ref else { return }
self.follow_state = .unfollows
}
}
}
struct GradientFollowButtonPreviews: View {
let target: FollowTarget = .pubkey("")
let target: FollowTarget = .pubkey(.empty)
var body: some View {
VStack {
Text(verbatim: "Unfollows")

View File

@ -135,8 +135,7 @@ struct CreateAccountView_Previews: PreviewProvider {
}
func KeyText(_ pubkey: Binding<Pubkey>) -> some View {
let decoded = hex_decode(pubkey.wrappedValue)!
let bechkey = bech32_encode(hrp: PUBKEY_HRP, decoded)
let bechkey = bech32_encode(hrp: PUBKEY_HRP, pubkey.wrappedValue.bytes)
return Text(bechkey)
.textSelection(.enabled)
.multilineTextAlignment(.center)

View File

@ -18,7 +18,7 @@ struct DMView: View {
var Mention: some View {
Group {
if let mention = first_eref_mention(ev: event, privkey: damus_state.keypair.privkey) {
BuilderEventView(damus: damus_state, event_id: mention.ref.id)
BuilderEventView(damus: damus_state, event_id: mention.ref)
} else {
EmptyView()
}

View File

@ -72,10 +72,10 @@ func should_show_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos
}
extension View {
func pubkey_context_menu(bech32_pubkey: Pubkey) -> some View {
func pubkey_context_menu(pubkey: Pubkey) -> some View {
return self.contextMenu {
Button {
UIPasteboard.general.string = bech32_pubkey
UIPasteboard.general.string = pubkey.npub
} label: {
Label(NSLocalizedString("Copy Account ID", comment: "Context menu option for copying the ID of the account that created the note."), image: "copy2")
}

View File

@ -22,7 +22,7 @@ struct EventTop: View {
func ProfileName(is_anon: Bool) -> some View {
let profile = state.profiles.lookup(id: self.pubkey)
let pk = is_anon ? "anon" : self.pubkey
let pk = is_anon ? ANON_PUBKEY : self.pubkey
return EventProfileName(pubkey: pk, profile: profile, damus: state, size: .normal)
}

View File

@ -37,9 +37,9 @@ func reply_desc(profiles: Profiles, event: NostrEvent, locale: Locale = Locale.c
return NSLocalizedString("Replying to self", bundle: bundle, comment: "Label to indicate that the user is replying to themself.")
}
let names: [String] = pubkeys.map {
let prof = profiles.lookup(id: $0)
return Profile.displayName(profile: prof, pubkey: $0).username.truncate(maxLength: 50)
let names: [String] = pubkeys.map { pk in
let prof = profiles.lookup(id: pk)
return Profile.displayName(profile: prof, pubkey: pk).username.truncate(maxLength: 50)
}
let uniqueNames = NSOrderedSet(array: names).array as! [String]

View File

@ -27,9 +27,7 @@ struct EventMenuContext: View {
var body: some View {
HStack {
Menu {
MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, muted_threads: muted_threads, settings: settings)
} label: {
Label("", systemImage: "ellipsis")
.foregroundColor(Color.gray)
@ -77,13 +75,13 @@ struct MenuItems: View {
}
Button {
UIPasteboard.general.string = bech32_pubkey(target_pubkey)
UIPasteboard.general.string = target_pubkey.npub
} label: {
Label(NSLocalizedString("Copy user public key", comment: "Context menu option for copying the ID of the user who created the note."), image: "user")
}
Button {
UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id
UIPasteboard.general.string = event.id.bech32
} label: {
Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book")
}

View File

@ -34,7 +34,7 @@ struct EventShell<Content: View>: View {
!options.contains(.no_action_bar)
}
func get_mention() -> Mention? {
func get_mention() -> Mention<NoteId>? {
if self.options.contains(.nested) || self.options.contains(.no_mentions) {
return nil
}
@ -42,8 +42,8 @@ struct EventShell<Content: View>: View {
return first_eref_mention(ev: event, privkey: state.keypair.privkey)
}
func Mention(_ mention: Mention) -> some View {
return BuilderEventView(damus: state, event_id: mention.ref.id)
func Mention(_ mention: Mention<NoteId>) -> some View {
return BuilderEventView(damus: state, event_id: mention.ref)
}
var ActionBar: some View {

View File

@ -20,12 +20,12 @@ struct LongformEvent {
for tag in ev.tags {
guard tag.count >= 2 else { continue }
switch tag[0] {
case "title": longform.title = tag[1]
case "image": longform.image = URL(string: tag[1])
case "summary": longform.summary = tag[1]
switch tag[0].string() {
case "title": longform.title = tag[1].string()
case "image": longform.image = URL(string: tag[1].string())
case "summary": longform.summary = tag[1].string()
case "published_at":
longform.published_at = Double(tag[1]).map { d in Date(timeIntervalSince1970: d) }
longform.published_at = Double(tag[1].string()).map { d in Date(timeIntervalSince1970: d) }
default:
break
}

View File

@ -50,7 +50,7 @@ struct SelectedEventView: View {
EventBody(damus_state: damus, event: event, size: size, options: [.wide])
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
BuilderEventView(damus: damus, event_id: mention.ref.id)
BuilderEventView(damus: damus, event_id: mention.ref)
.padding(.horizontal)
}

View File

@ -59,7 +59,7 @@ struct TextEvent: View {
func event_has_tag(ev: NostrEvent, tag: String) -> Bool {
for t in ev.tags {
if t.count >= 1 && t[0] == tag {
if t.count >= 1 && t[0].matches_str(tag) {
return true
}
}

View File

@ -31,18 +31,16 @@ struct FollowButtonView: View {
.stroke(follow_state == .unfollows ? .clear : borderColor(), lineWidth: 1)
}
}
.onReceive(handle_notify(.followed)) { pk in
guard pk.key == "p", target.pubkey == pk.ref_id else {
return
}
.onReceive(handle_notify(.followed)) { follow in
guard case .pubkey(let pk) = follow,
pk == target.pubkey else { return }
self.follow_state = .follows
}
.onReceive(handle_notify(.unfollowed)) { pk in
guard pk.key == "p", target.pubkey == pk.ref_id else {
return
}
.onReceive(handle_notify(.unfollowed)) { unfollow in
guard case .pubkey(let pk) = unfollow,
pk == target.pubkey else { return }
self.follow_state = .unfollows
}
}
@ -65,7 +63,7 @@ struct FollowButtonView: View {
}
struct FollowButtonPreviews: View {
let target: FollowTarget = .pubkey("")
let target: FollowTarget = .pubkey(test_pubkey)
var body: some View {
VStack {
Text(verbatim: "Unfollows")

View File

@ -146,10 +146,8 @@ func parse_key(_ thekey: String) -> ParsedKey? {
if let bech_key = decode_bech32_key(key) {
switch bech_key {
case .pub(let pk):
return .pub(pk)
case .sec(let sec):
return .priv(sec)
case .pub(let pk): return .pub(pk)
case .sec(let sec): return .priv(sec)
}
}
@ -195,11 +193,12 @@ func process_login(_ key: ParsedKey, is_pubkey: Bool) async throws {
save_pubkey(pubkey: nip05.pubkey)
case .hex(let hexstr):
if is_pubkey {
if is_pubkey, let pubkey = hex_decode_pubkey(hexstr) {
try clear_saved_privkey()
save_pubkey(pubkey: hexstr)
} else {
try handle_privkey(hexstr)
save_pubkey(pubkey: pubkey)
} else if let privkey = hex_decode_privkey(hexstr) {
try handle_privkey(privkey)
}
}
@ -209,10 +208,8 @@ func process_login(_ key: ParsedKey, is_pubkey: Bool) async throws {
guard let pk = privkey_to_pubkey(privkey: privkey) else {
throw LoginError.invalid_key
}
if let pub = bech32_pubkey(pk), let priv = bech32_privkey(privkey) {
CredentialHandler().save_credential(pubkey: pub, privkey: priv)
}
CredentialHandler().save_credential(pubkey: pk, privkey: privkey)
save_pubkey(pubkey: pk)
}
@ -232,7 +229,7 @@ struct NIP05Result: Decodable {
struct NIP05User {
let pubkey: Pubkey
let relays: [String]
//let relays: [String]
}
func get_nip05_pubkey(id: String) async -> NIP05User? {
@ -245,30 +242,24 @@ func get_nip05_pubkey(id: String) async -> NIP05User? {
let user = parts[0]
let host = parts[1]
guard let url = URL(string: "https://\(host)/.well-known/nostr.json?name=\(user)") else {
return nil
}
guard let (data, _) = try? await URLSession.shared.data(for: URLRequest(url: url)) else {
return nil
}
guard let json: NIP05Result = decode_data(data) else {
return nil
}
guard let pubkey = json.names[user] else {
guard let url = URL(string: "https://\(host)/.well-known/nostr.json?name=\(user)"),
let (data, _) = try? await URLSession.shared.data(for: URLRequest(url: url)),
let json: NIP05Result = decode_data(data),
let pubkey_hex = json.names[user],
let pubkey = hex_decode_pubkey(pubkey_hex)
else {
return nil
}
/*
var relays: [String] = []
if let rs = json.relays {
if let rs = rs[pubkey] {
relays = rs
}
}
return NIP05User(pubkey: pubkey, relays: relays)
if let rs = json.relays, let rs = rs[pubkey] {
relays = rs
}
*/
return NIP05User(pubkey: pubkey/*, relays: relays*/)
}
struct KeyInput: View {

View File

@ -13,15 +13,12 @@ struct MutelistView: View {
func RemoveAction(pubkey: Pubkey) -> some View {
Button {
guard let mutelist = damus_state.contacts.mutelist else {
return
}
guard let keypair = damus_state.keypair.to_full() else {
return
}
guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: pubkey) else {
guard let mutelist = damus_state.contacts.mutelist,
let keypair = damus_state.keypair.to_full(),
let new_ev = remove_from_mutelist(keypair: keypair,
prev: mutelist,
to_remove: .pubkey(pubkey))
else {
return
}
@ -48,22 +45,15 @@ struct MutelistView: View {
}
.navigationTitle(NSLocalizedString("Muted Users", comment: "Navigation title of view to see list of muted users."))
.onAppear {
users = get_mutelist_users(damus_state.contacts.mutelist)
users = get_mutelist_users(damus_state.contacts.mutelist)
}
}
}
func get_mutelist_users(_ mlist: NostrEvent?) -> [String] {
guard let mutelist = mlist else {
return []
}
return mutelist.tags.reduce(into: Array<String>()) { pks, tag in
if tag.count >= 2 && tag[0] == "p" {
pks.append(tag[1])
}
}
func get_mutelist_users(_ mutelist: NostrEvent?) -> Array<Pubkey> {
guard let mutelist else { return [] }
return Array(mutelist.referenced_pubkeys)
}
struct MutelistView_Previews: PreviewProvider {

View File

@ -230,7 +230,7 @@ struct NoteContentView: View {
for block in blocks.blocks {
switch block {
case .mention(let m):
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
if case .pubkey(let pk) = m.ref, pk == profile.pubkey {
load(force_artifacts: true)
return
}
@ -265,21 +265,21 @@ func url_str(_ url: URL) -> CompatibleText {
return CompatibleText(attributed: attributedString)
}
func mention_str(_ m: Mention, profiles: Profiles) -> CompatibleText {
switch m.type {
case .pubkey:
let pk = m.ref.ref_id
func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText {
switch m.ref {
case .pubkey(let pk):
let npub = bech32_pubkey(pk)
let profile = profiles.lookup(id: pk)
let disp = Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50)
var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))")
attributedString.link = URL(string: "damus:nostr:\(npub)")
attributedString.foregroundColor = DamusColors.purple
return CompatibleText(attributed: attributedString)
case .event:
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
case .note(let note_id):
let bevid = bech32_note_id(note_id)
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
attributedString.link = URL(string: "damus:\(encode_event_id_uri(m.ref))")
attributedString.link = URL(string: "damus:nostr:\(bevid)")
attributedString.foregroundColor = DamusColors.purple
return CompatibleText(attributed: attributedString)
@ -394,7 +394,7 @@ func note_artifact_is_separated(kind: NostrKind?) -> Bool {
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: Privkey?) -> NoteArtifacts {
let blocks = ev.blocks(privkey)
if ev.known_kind == .longform {
return .longform(LongformContent(ev.content))
}
@ -427,7 +427,9 @@ func reduce_text_block(blocks: [Block], ind: Int, txt: String, one_note_ref: Boo
if let next = blocks[safe: ind+1] {
if case .url(let u) = next, classify_url(u).is_media != nil {
trimmed = trim_suffix(trimmed)
} else if case .mention(let m) = next, m.type == .event, one_note_ref {
} else if case .mention(let m) = next,
case .note = m.ref,
one_note_ref {
trimmed = trim_suffix(trimmed)
}
}
@ -450,7 +452,7 @@ func render_blocks(blocks bs: Blocks, profiles: Profiles) -> NoteArtifactsSepara
switch block {
case .mention(let m):
if m.type == .event && one_note_ref {
if case .note = m.ref, one_note_ref {
return str
}
return str + mention_str(m, profiles: profiles)

View File

@ -10,10 +10,10 @@ import SwiftUI
struct ParticipantsView: View {
let damus_state: DamusState
@Binding var references: [ReferencedId]
@Binding var originalReferences: [ReferencedId]
let original_pubkeys: [Pubkey]
@Binding var filtered_pubkeys: Set<Pubkey>
var body: some View {
VStack {
Text("Replying to", comment: "Text indicating that the view is used for editing which participants are replied to in a note.")
@ -23,7 +23,7 @@ struct ParticipantsView: View {
Button {
// Remove all "p" refs, keep "e" refs
references = originalReferences.eRefs
filtered_pubkeys = Set(original_pubkeys)
} label: {
Text("Remove all", comment: "Button label to remove all participants from a note reply.")
}
@ -34,7 +34,7 @@ struct ParticipantsView: View {
.clipShape(Capsule())
Button {
references = originalReferences
filtered_pubkeys = []
} label: {
Text("Add all", comment: "Button label to re-add all original participants as profiles to reply to in a note")
}
@ -48,26 +48,19 @@ struct ParticipantsView: View {
}
VStack {
ScrollView {
ForEach(originalReferences.pRefs) { participant in
let pubkey = participant.id
ForEach(original_pubkeys) { pubkey in
HStack {
UserView(damus_state: damus_state, pubkey: pubkey)
Image("check-circle.fill")
.font(.system(size: 30))
.foregroundColor(references.contains(participant) ? DamusColors.purple : .gray)
.foregroundColor(filtered_pubkeys.contains(pubkey) ? .gray : DamusColors.purple)
}
.onTapGesture {
if references.contains(participant) {
references = references.filter {
$0 != participant
}
if filtered_pubkeys.contains(pubkey) {
filtered_pubkeys.remove(pubkey)
} else {
if references.contains(participant) {
// Don't add it twice
} else {
references.append(participant)
}
filtered_pubkeys.insert(pubkey)
}
}
}

View File

@ -50,8 +50,8 @@ struct PostView: View {
@State var error: String? = nil
@State var uploadedMedias: [UploadedMedia] = []
@State var image_upload_confirm: Bool = false
@State var originalReferences: [ReferencedId] = []
@State var references: [ReferencedId] = []
@State var references: [RefId] = []
@State var filtered_pubkeys: Set<Pubkey> = []
@State var focusWordAttributes: (String?, NSRange?) = (nil, nil)
@State var newCursorIndex: Int?
@State var postTextViewCanScroll: Bool = true
@ -76,7 +76,13 @@ struct PostView: View {
}
func send_post() {
let new_post = build_post(post: self.post, action: action, uploadedMedias: uploadedMedias, references: references)
let refs = references.filter { ref in
if case .pubkey(let pk) = ref, filtered_pubkeys.contains(pk) {
return false
}
return true
}
let new_post = build_post(post: self.post, action: action, uploadedMedias: uploadedMedias, references: refs)
notify(.post(.post(new_post)))
@ -155,8 +161,7 @@ struct PostView: View {
}
let profile = damus_state.profiles.lookup(id: pubkey)
let bech32_pubkey = bech32_pubkey(pubkey) ?? ""
return user_tag_attr_string(profile: profile, pubkey: bech32_pubkey)
return user_tag_attr_string(profile: profile, pubkey: pubkey)
}
func clear_draft() {
@ -310,7 +315,17 @@ struct PostView: View {
self.post = initialString()
self.tagModel.diff = post.string.count
}
var pubkeys: [Pubkey] {
self.references.reduce(into: [Pubkey]()) { pks, ref in
guard case .pubkey(let pk) = ref else {
return
}
pks.append(pk)
}
}
var body: some View {
GeometryReader { (deviceSize: GeometryProxy) in
VStack(alignment: .leading, spacing: 0) {
@ -321,7 +336,7 @@ struct PostView: View {
ScrollViewReader { scroller in
ScrollView {
if case .replying_to(let replying_to) = self.action {
ReplyView(replying_to: replying_to, damus: damus_state, originalReferences: $originalReferences, references: $references)
ReplyView(replying_to: replying_to, damus: damus_state, original_pubkeys: pubkeys, filtered_pubkeys: $filtered_pubkeys)
}
Editor(deviceSize: deviceSize)
@ -385,10 +400,8 @@ struct PostView: View {
switch action {
case .replying_to(let replying_to):
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
originalReferences = references
case .quoting(let quoting):
references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
originalReferences = references
case .posting(let target):
guard !loaded_draft else { break }
@ -551,13 +564,7 @@ func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts?
}
func build_post(post: NSMutableAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [ReferencedId]) -> NostrPost {
var kind: NostrKind = .text
if case .replying_to(let ev) = action, ev.known_kind == .chat {
kind = .chat
}
func build_post(post: NSMutableAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId]) -> NostrPost {
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
if let link = attributes[.link] as? String {
let normalized_link: String
@ -586,9 +593,9 @@ func build_post(post: NSMutableAttributedString, action: PostAction, uploadedMed
content.append(" " + imagesString + " ")
}
if case .quoting(let ev) = action, let id = bech32_note_id(ev.id) {
content.append(" nostr:" + id)
if case .quoting(let ev) = action {
content.append(" nostr:" + bech32_note_id(ev.id))
}
return NostrPost(content: content, references: references, kind: kind, tags: img_meta_tags)
return NostrPost(content: content, references: references, kind: .text, tags: img_meta_tags)
}

View File

@ -31,10 +31,7 @@ struct UserSearch: View {
}
func on_user_tapped(user: SearchedUser) {
guard let pk = bech32_pubkey(user.pubkey) else {
return
}
let pk = user.pubkey
let user_tag = user_tag_attr_string(profile: user.profile, pubkey: pk)
appendUserTag(withTag: user_tag)
@ -159,7 +156,7 @@ func user_tag_attr_string(profile: Profile?, pubkey: Pubkey) -> NSMutableAttribu
return NSMutableAttributedString(string: tagString, attributes: [
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0),
NSAttributedString.Key.foregroundColor: UIColor.label,
NSAttributedString.Key.link: "damus:nostr:\(pubkey)"
NSAttributedString.Key.link: "damus:nostr:\(pubkey.npub)"
])
}

View File

@ -39,7 +39,7 @@ struct AboutView: View {
}
}
.onAppear {
let blocks = parse_note_content(content: about, tags: [])
let blocks = parse_note_content(content: .content(about, nil))
about_string = render_blocks(blocks: blocks, profiles: state.profiles).content.attributed
}

View File

@ -45,7 +45,7 @@ struct ProfileNameView: View {
Spacer()
KeyView(pubkey: pubkey)
.pubkey_context_menu(bech32_pubkey: pubkey)
.pubkey_context_menu(pubkey: pubkey)
}
}
}

View File

@ -42,7 +42,8 @@ struct EditProfilePictureView: View {
private func get_profile_url() -> URL? {
if let profile_url {
return profile_url
} else if let state = damus_state, let picture = state.profiles.lookup(id: pubkey)?.picture {
} else if let state = damus_state,
let picture = state.profiles.lookup(id: pubkey)?.picture {
return URL(string: picture)
} else {
return profile_url ?? URL(string: robohash(pubkey))

View File

@ -190,7 +190,7 @@ struct ProfileView: View {
return
}
guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: profile.pubkey) else {
guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .pubkey(profile.pubkey)) else {
return
}
@ -260,10 +260,11 @@ struct ProfileView: View {
func actionSection(profile_data: Profile?) -> some View {
return Group {
if let profile = profile_data {
if let lnurl = profile.lnurl, lnurl != "" {
lnButton(lnurl: lnurl, profile: profile)
}
if let profile = profile_data,
let lnurl = profile.lnurl,
lnurl != ""
{
lnButton(lnurl: lnurl, profile: profile)
}
dmButton
@ -353,7 +354,7 @@ struct ProfileView: View {
HStack {
if let contact = profile.contacts {
let contacts = contact.referenced_pubkeys.map { $0.ref_id }
let contacts = Array(contact.referenced_pubkeys)
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
NavigationLink(value: Route.Following(following: following_model)) {
HStack {
@ -466,11 +467,8 @@ struct ProfileView: View {
// our profilemodel needs a bit more help
}
.sheet(isPresented: $show_share_sheet) {
if let npub = bech32_pubkey(profile.pubkey) {
if let url = URL(string: "https://damus.io/" + npub) {
ShareSheet(activityItems: [url])
}
}
let url = URL(string: "https://damus.io/" + profile.pubkey.npub)!
ShareSheet(activityItems: [url])
}
.fullScreenCover(isPresented: $show_qr_code) {
QRCodeView(damus_state: damus_state, pubkey: profile.pubkey)
@ -517,7 +515,7 @@ struct KeyView: View {
}
var body: some View {
let bech32 = bech32_pubkey(pubkey) ?? pubkey
let bech32 = pubkey.npub
HStack {
Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))")

View File

@ -10,9 +10,13 @@ import CoreImage.CIFilterBuiltins
struct ProfileScanResult: Equatable {
let pubkey: Pubkey
init(hex: String) {
self.pubkey = hex
init?(hex: String) {
guard let pk = hex_decode(hex).map({ bytes in Pubkey(Data(bytes)) }) else {
return nil
}
self.pubkey = pk
}
init?(string: String) {
@ -25,14 +29,17 @@ struct ProfileScanResult: Equatable {
str.removeFirst("nostr:".count)
}
if let _ = hex_decode(str), str.count == 64 {
self = .init(hex: str)
if let decoded = hex_decode(str),
str.count == 64
{
self.pubkey = Pubkey(Data(decoded))
return
}
if str.starts(with: "npub"), let b32 = try? bech32_decode(str) {
let hex = hex_encode(b32.data)
self = .init(hex: hex)
if str.starts(with: "npub"),
let b32 = try? bech32_decode(str)
{
self.pubkey = Pubkey(b32.data)
return
}
@ -56,14 +63,6 @@ struct QRCodeView: View {
let generator = UIImpactFeedbackGenerator(style: .light)
var maybe_key: String? {
guard let key = bech32_pubkey(pubkey) else {
return nil
}
return key
}
@ViewBuilder
func navImage(systemImage: String) -> some View {
Image(systemName: systemImage)
@ -143,18 +142,16 @@ struct QRCodeView: View {
Spacer()
if let key = maybe_key {
Image(uiImage: generateQRCode(pubkey: "nostr:" + key))
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: 300, height: 300)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.white, lineWidth: 5.0))
.shadow(radius: 10)
}
Image(uiImage: generateQRCode(pubkey: "nostr:" + pubkey.npub))
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: 300, height: 300)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.white, lineWidth: 5.0))
.shadow(radius: 10)
Spacer()
Text("Follow me on Nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")

View File

@ -91,6 +91,7 @@ struct RelayDetailView: View {
}
}
}
if let relay_connection {
Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) {
HStack {
@ -101,7 +102,7 @@ struct RelayDetailView: View {
}
}
if let nip11 = nip11 {
if let nip11 {
if nip11.is_paid {
Section(content: {
RelayPaidDetail(payments_url: nip11.payments_url)
@ -172,7 +173,7 @@ struct RelayDetailView: View {
struct RelayDetailView_Previews: PreviewProvider {
static var previews: some View {
let metadata = RelayMetadata(name: "name", description: "desc", pubkey: "pubkey", contact: "contact", supported_nips: [1,2,3], software: "software", version: "version", limitation: Limitations.empty, payments_url: "https://jb55.com")
let metadata = RelayMetadata(name: "name", description: "desc", pubkey: test_pubkey, contact: "contact", supported_nips: [1,2,3], software: "software", version: "version", limitation: Limitations.empty, payments_url: "https://jb55.com")
RelayDetailView(state: test_damus_state(), relay: "relay", nip11: metadata)
}
}

View File

@ -93,7 +93,7 @@ struct RelayView: View {
}
}
func RemoveButton(privkey: String, showText: Bool) -> some View {
func RemoveButton(privkey: Privkey, showText: Bool) -> some View {
Button(action: {
guard let ev = state.contacts.event else {
return

View File

@ -10,17 +10,23 @@ import SwiftUI
struct ReplyView: View {
let replying_to: NostrEvent
let damus: DamusState
@Binding var originalReferences: [ReferencedId]
@Binding var references: [ReferencedId]
let original_pubkeys: [Pubkey]
@Binding var filtered_pubkeys: Set<Pubkey>
@State var participantsShown: Bool = false
var references: [Pubkey] {
original_pubkeys.filter { pk in
!filtered_pubkeys.contains(pk)
}
}
var ReplyingToSection: some View {
HStack {
Group {
let names = references.pRefs
let names = references
.map { pubkey in
let pk = pubkey.ref_id
let pk = pubkey
let prof = damus.profiles.lookup(id: pk)
return "@" + Profile.displayName(profile: prof, pubkey: pk).username.truncate(maxLength: 50)
}
@ -40,11 +46,15 @@ struct ReplyView: View {
}
.sheet(isPresented: $participantsShown) {
if #available(iOS 16.0, *) {
ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences)
ParticipantsView(damus_state: damus,
original_pubkeys: self.original_pubkeys,
filtered_pubkeys: $filtered_pubkeys)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
} else {
ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences)
ParticipantsView(damus_state: damus,
original_pubkeys: self.original_pubkeys,
filtered_pubkeys: $filtered_pubkeys)
}
}
.padding(.leading, 75)
@ -81,10 +91,16 @@ struct ReplyView: View {
struct ReplyView_Previews: PreviewProvider {
static var previews: some View {
VStack {
ReplyView(replying_to: test_note, damus: test_damus_state(), originalReferences: .constant([]), references: .constant([]))
ReplyView(replying_to: test_note,
damus: test_damus_state(),
original_pubkeys: [],
filtered_pubkeys: .constant([]))
.frame(height: 300)
ReplyView(replying_to: test_longform_event.event, damus: test_damus_state(), originalReferences: .constant([]), references: .constant([]))
ReplyView(replying_to: test_longform_event.event,
damus: test_damus_state(),
original_pubkeys: [],
filtered_pubkeys: .constant([]))
.frame(height: 300)
}
}

View File

@ -51,12 +51,8 @@ struct ReportView: View {
return
}
guard let note_id = bech32_note_id(ev.id) else {
return
}
report_sent = true
report_id = note_id
report_id = bech32_note_id(ev.id)
}
var send_report_button_text: String {
@ -131,9 +127,9 @@ struct ReportView_Previews: PreviewProvider {
let ds = test_damus_state()
VStack {
ReportView(postbox: ds.postbox, target: ReportTarget.user(""), keypair: test_keypair.to_full()!)
ReportView(postbox: ds.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!)
ReportView(postbox: ds.postbox, target: ReportTarget.user(""), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id")
ReportView(postbox: ds.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id")
}
}

View File

@ -38,7 +38,7 @@ struct SaveKeysView: View {
Text("This is your account ID, you can give this to your friends so that they can follow you. Tap to copy.", comment: "Label to describe that a public key is the user's account ID and what they can do with it.")
.padding(.bottom, 10)
SaveKeyView(text: account.pubkey_bech32, textContentType: .username, is_copied: $pub_copied, focus: $pubkey_focused)
SaveKeyView(text: account.pubkey.npub, textContentType: .username, is_copied: $pub_copied, focus: $pubkey_focused)
.padding(.bottom, 10)
if pub_copied {
@ -49,7 +49,7 @@ struct SaveKeysView: View {
Text("This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!", comment: "Label to describe that a private key is the user's secret account key and what they should do with it.")
.padding(.bottom, 10)
SaveKeyView(text: account.privkey_bech32, textContentType: .newPassword, is_copied: $priv_copied, focus: $privkey_focused)
SaveKeyView(text: account.privkey.nsec, textContentType: .newPassword, is_copied: $priv_copied, focus: $privkey_focused)
.padding(.bottom, 10)
}
@ -115,8 +115,8 @@ struct SaveKeysView: View {
self.pool.register_handler(sub_id: "signup", handler: handle_event)
credential_handler.save_credential(pubkey: account.pubkey_bech32, privkey: account.privkey_bech32)
credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey)
self.loading = true
self.pool.connect()

View File

@ -14,15 +14,14 @@ enum SearchState {
case not_found
}
enum SearchType {
case event
case profile
case nip05
enum SearchType: Equatable {
case event(NoteId)
case profile(Pubkey)
case nip05(String)
}
struct SearchingEventView: View {
let state: DamusState
let evid: String
let search_type: SearchType
@State var search_state: SearchState = .searching
@ -38,18 +37,18 @@ struct SearchingEventView: View {
}
}
func handle_search(_ evid: String) {
func handle_search(search: SearchType) {
self.search_state = .searching
switch search_type {
case .nip05:
if let pk = state.profiles.nip05_pubkey[evid] {
switch search {
case .nip05(let nip05):
if let pk = state.profiles.nip05_pubkey[nip05] {
if state.profiles.lookup(id: pk) != nil {
self.search_state = .found_profile(pk)
}
} else {
Task {
guard let nip05 = NIP05.parse(evid) else {
guard let nip05 = NIP05.parse(nip05) else {
self.search_state = .not_found
return
}
@ -71,16 +70,16 @@ struct SearchingEventView: View {
}
}
case .event:
find_event(state: state, query: .event(evid: evid)) { res in
case .event(let note_id):
find_event(state: state, query: .event(evid: note_id)) { res in
guard case .event(let ev) = res else {
self.search_state = .not_found
return
}
self.search_state = .found(ev)
}
case .profile:
find_event(state: state, query: .profile(pubkey: evid)) { res in
case .profile(let pubkey):
find_event(state: state, query: .profile(pubkey: pubkey)) { res in
guard case .profile(_, let ev) = res else {
self.search_state = .not_found
return
@ -113,11 +112,11 @@ struct SearchingEventView: View {
Text("\(search_name) not found", comment: "When a note or profile is not found when searching for it via its note id")
}
}
.onChange(of: evid, debounceTime: 0.5) { evid in
handle_search(evid)
.onChange(of: search_type, debounceTime: 0.5) { stype in
handle_search(search: stype)
}
.onAppear {
handle_search(evid)
handle_search(search: search_type)
}
}
}
@ -125,6 +124,6 @@ struct SearchingEventView: View {
struct SearchingEventView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
SearchingEventView(state: state, evid: test_note.id, search_type: .event)
SearchingEventView(state: state, search_type: .event(test_note.id))
}
}

View File

@ -18,7 +18,7 @@ enum Search: Identifiable {
case profile(Pubkey)
case note(NoteId)
case nip05(String)
case hex(String)
case hex(Data)
case multi(MultiSearch)
var id: String {
@ -67,28 +67,22 @@ struct InnerSearchResults: View {
HashtagSearch(ht)
case .nip05(let addr):
SearchingEventView(state: damus_state, evid: addr, search_type: .nip05)
case .profile(let prof):
let decoded = try? bech32_decode(prof)
let hex = hex_encode(decoded!.data)
SearchingEventView(state: damus_state, evid: hex, search_type: .profile)
SearchingEventView(state: damus_state, search_type: .nip05(addr))
case .profile(let pubkey):
SearchingEventView(state: damus_state, search_type: .profile(pubkey))
case .hex(let h):
//let prof_view = ProfileView(damus_state: damus_state, pubkey: h)
//let ev_view = ThreadView(damus: damus_state, event_id: h)
VStack(spacing: 10) {
SearchingEventView(state: damus_state, evid: h, search_type: .event)
SearchingEventView(state: damus_state, evid: h, search_type: .profile)
SearchingEventView(state: damus_state, search_type: .event(NoteId(h)))
SearchingEventView(state: damus_state, search_type: .profile(Pubkey(h)))
}
case .note(let nid):
let decoded = try? bech32_decode(nid)
let hex = hex_encode(decoded!.data)
SearchingEventView(state: damus_state, evid: hex, search_type: .event)
SearchingEventView(state: damus_state, search_type: .event(nid))
case .multi(let multi):
VStack {
HashtagSearch(multi.hashtag)
@ -146,20 +140,18 @@ func search_for_string(profiles: Profiles, _ new: String) -> Search? {
return .hashtag(make_hashtagable(new))
}
if hex_decode(new) != nil, new.count == 64 {
if let new = hex_decode_id(new) {
return .hex(new)
}
if new.starts(with: "npub") {
if (try? bech32_decode(new)) != nil {
return .profile(new)
if let decoded = bech32_pubkey_decode(new) {
return .profile(decoded)
}
}
if new.starts(with: "note") {
if (try? bech32_decode(new)) != nil {
return .note(new)
}
if new.starts(with: "note"), let decoded = try? bech32_decode(new) {
return .note(NoteId(decoded.data))
}
let multisearch = MultiSearch(hashtag: make_hashtagable(new), profiles: search_profiles(profiles: profiles, search: new))
@ -181,13 +173,19 @@ func make_hashtagable(_ str: String) -> String {
func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] {
// Search by hex pubkey.
if search.count == 64 && hex_decode(search) != nil, let profile = profiles.lookup(id: search) {
return [SearchedUser(profile: profile, pubkey: search)]
if let pubkey = hex_decode_pubkey(search),
let profile = profiles.lookup(id: pubkey)
{
return [SearchedUser(profile: profile, pubkey: pubkey)]
}
// Search by npub pubkey.
if search.starts(with: "npub"), let bech32_key = decode_bech32_key(search), case Bech32Key.pub(let hex) = bech32_key, let profile = profiles.lookup(id: hex) {
return [SearchedUser(profile: profile, pubkey: hex)]
if search.starts(with: "npub"),
let bech32_key = decode_bech32_key(search),
case Bech32Key.pub(let pk) = bech32_key,
let profile = profiles.lookup(id: pk)
{
return [SearchedUser(profile: profile, pubkey: pk)]
}
let new = search.lowercased()

View File

@ -14,7 +14,7 @@ struct ThreadView: View {
@Environment(\.dismiss) var dismiss
var parent_events: [NostrEvent] {
state.events.parent_events(event: thread.event)
state.events.parent_events(event: thread.event, privkey: state.keypair.privkey)
}
var child_events: [NostrEvent] {
@ -34,7 +34,7 @@ struct ThreadView: View {
selected: false)
.padding(.horizontal)
.onTapGesture {
thread.set_active_event(parent_event)
thread.set_active_event(parent_event, privkey: self.state.keypair.privkey)
scroll_to_event(scroller: reader, id: parent_event.id, delay: 0.1, animate: false)
}
@ -77,7 +77,7 @@ struct ThreadView: View {
)
.padding(.horizontal)
.onTapGesture {
thread.set_active_event(child_event)
thread.set_active_event(child_event, privkey: state.keypair.privkey)
scroll_to_event(scroller: reader, id: child_event.id, delay: 0.1, animate: false)
}

View File

@ -192,7 +192,7 @@ struct WalletView: View {
}
}
let test_wallet_connect_url = WalletConnectURL(pubkey: "pk", relay: .init("wss://relay.damus.io")!, keypair: test_damus_state().keypair.to_full()!, lud16: "jb55@sendsats.com")
let test_wallet_connect_url = WalletConnectURL(pubkey: test_pubkey, relay: .init("wss://relay.damus.io")!, keypair: test_damus_state().keypair.to_full()!, lud16: "jb55@sendsats.com")
struct WalletView_Previews: PreviewProvider {
static let tds = test_damus_state()

View File

@ -16,7 +16,7 @@ struct ZapsView: View {
init(state: DamusState, target: ZapTarget) {
self.state = state
self.model = ZapsModel(state: state, target: target)
self._zaps = ObservedObject(wrappedValue: state.events.get_cache_data(target.id).zaps_model)
self._zaps = ObservedObject(wrappedValue: state.events.get_cache_data(NoteId(target.id)).zaps_model)
}
var body: some View {
@ -40,6 +40,6 @@ struct ZapsView: View {
struct ZapsView_Previews: PreviewProvider {
static var previews: some View {
ZapsView(state: test_damus_state(), target: .profile("pk"))
ZapsView(state: test_damus_state(), target: .profile(test_pubkey))
}
}

View File

@ -25,20 +25,14 @@ class Bech32Tests: XCTestCase {
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
let pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
guard let b32_pubkey = bech32_pubkey(pubkey) else {
let pubkey = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
guard let decoded = try? bech32_decode(pubkey.npub) else {
XCTAssert(false)
return
}
guard let decoded = try? bech32_decode(b32_pubkey) else {
XCTAssert(false)
return
}
let encoded = hex_encode(decoded.data)
XCTAssertEqual(encoded, pubkey)
XCTAssertEqual(decoded.data, pubkey.id)
}
func testPerformanceExample() throws {

View File

@ -11,96 +11,97 @@ import XCTest
final class DMTests: XCTestCase {
var alice: Keypair {
let sec = "494c680d20f202807a116a6915815bd76a27d62802e7585806f6a2e034cb5cdb"
let pk = "22d925632551a3299022e98de7f9c1087f79a21209f3413ec24ec219b08bd1e4"
let sec = hex_decode_privkey("494c680d20f202807a116a6915815bd76a27d62802e7585806f6a2e034cb5cdb")!
let pk = hex_decode_pubkey("22d925632551a3299022e98de7f9c1087f79a21209f3413ec24ec219b08bd1e4")!
return Keypair(pubkey: pk, privkey: sec)
}
var bob: Keypair {
let sec = "aa8920b05b4bd5c79fce46868ed5ebc82bdb91b211850b14541bfbd13953cfef"
let pk = "5a9a277dca94260688ecf7d63053de8c121b7f01f609d7f84a1eb9cff64e4606"
let sec = hex_decode_privkey("aa8920b05b4bd5c79fce46868ed5ebc82bdb91b211850b14541bfbd13953cfef")!
let pk = hex_decode_pubkey("5a9a277dca94260688ecf7d63053de8c121b7f01f609d7f84a1eb9cff64e4606")!
return Keypair(pubkey: pk, privkey: sec)
}
var charlie: Keypair {
let sec = "4c79130952c9c3b017dad62f37f285853a9c53f2a1184d94594f5b860f30b5a5"
let pk = "51c0d263fbfc4bf850805dccf9a29125071e6fed9619bff3efa9a6b5bbcc54a7"
let sec = hex_decode_privkey("4c79130952c9c3b017dad62f37f285853a9c53f2a1184d94594f5b860f30b5a5")!
let pk = hex_decode_pubkey("51c0d263fbfc4bf850805dccf9a29125071e6fed9619bff3efa9a6b5bbcc54a7")!
return Keypair(pubkey: pk, privkey: sec)
}
var dave: Keypair {
let sec = "630ffd518084334cbb9ecb20d9532ce0658b8123f4ba565c236d0cea9a4a2cfe"
let pk = "b42e44b555013239a0d5dcdb09ebde0857cd8a5a57efbba5a2b6ac78833cb9f0"
let sec = hex_decode_privkey("630ffd518084334cbb9ecb20d9532ce0658b8123f4ba565c236d0cea9a4a2cfe")!
let pk = hex_decode_pubkey("b42e44b555013239a0d5dcdb09ebde0857cd8a5a57efbba5a2b6ac78833cb9f0")!
return Keypair(pubkey: pk, privkey: sec)
}
var fiatjaf: Keypair {
let sec = "5426893eab32191ec17a83a583d5c8f85adaabcab0fa56af277ea0b61f575599"
let pub = "e27258d7be6d84038967334bfd0954f05801b1bcd85b2afa4c03cfd16ae4b0ad"
let sec = hex_decode_privkey("5426893eab32191ec17a83a583d5c8f85adaabcab0fa56af277ea0b61f575599")!
let pub = hex_decode_pubkey("e27258d7be6d84038967334bfd0954f05801b1bcd85b2afa4c03cfd16ae4b0ad")!
return Keypair(pubkey: pub, privkey: sec)
}
/*
func testDMSortOrder() throws {
let notif = NewEventsBits()
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let pubkey = hex_decode_pubkey("3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681")!
let model = DirectMessagesModel(our_pubkey: pubkey)
let now = UInt32(Date().timeIntervalSince1970)
let alice_to_bob = create_dm("hi bob", to_pk: bob.pubkey, tags: [["p", bob.pubkey]], keypair: alice, created_at: now)!
let debouncer = Debouncer(interval: 3.0)
let alice_to_bob = create_dm("hi bob", to_pk: bob.pubkey, tags: [bob.pubkey.tag], keypair: alice, created_at: now)!
handle_incoming_dms(debouncer: debouncer, prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [alice_to_bob])
XCTAssertEqual(model.dms.count, 1)
XCTAssertEqual(model.dms[0].pubkey, bob.pubkey)
let bob_to_alice = create_dm("hi alice", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: bob, created_at: now + 1)!
let bob_to_alice = create_dm("hi alice", to_pk: alice.pubkey, tags: [alice.pubkey.tag], keypair: bob, created_at: now + 1)!
handle_incoming_dms(debouncer: debouncer, prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [bob_to_alice])
XCTAssertEqual(model.dms.count, 1)
XCTAssertEqual(model.dms[0].pubkey, bob.pubkey)
let alice_to_bob_2 = create_dm("hi bob", to_pk: bob.pubkey, tags: [["p", bob.pubkey]], keypair: alice, created_at: now + 2)!
let alice_to_bob_2 = create_dm("hi bob", to_pk: bob.pubkey, tags: [bob.pubkey.tag], keypair: alice, created_at: now + 2)!
handle_incoming_dms(debouncer: debouncer, prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [alice_to_bob_2])
XCTAssertEqual(model.dms.count, 1)
XCTAssertEqual(model.dms[0].pubkey, bob.pubkey)
let fiatjaf_to_alice = create_dm("hi alice", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: fiatjaf, created_at: now+5)!
let fiatjaf_to_alice = create_dm("hi alice", to_pk: alice.pubkey, tags: [alice.pubkey.tag], keypair: fiatjaf, created_at: now+5)!
handle_incoming_dms(debouncer: debouncer, prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [fiatjaf_to_alice])
XCTAssertEqual(model.dms.count, 2)
XCTAssertEqual(model.dms[0].pubkey, fiatjaf.pubkey)
let dave_to_alice = create_dm("hi alice", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: dave, created_at: now + 10)!
let dave_to_alice = create_dm("hi alice", to_pk: alice.pubkey, tags: [alice.pubkey.tag], keypair: dave, created_at: now + 10)!
handle_incoming_dms(debouncer: debouncer, prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [dave_to_alice])
XCTAssertEqual(model.dms.count, 3)
XCTAssertEqual(model.dms[0].pubkey, dave.pubkey)
let bob_to_alice_2 = create_dm("hi alice 2", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: bob, created_at: now + 15)!
let bob_to_alice_2 = create_dm("hi alice 2", to_pk: alice.pubkey, tags: [alice.pubkey.tag], keypair: bob, created_at: now + 15)!
handle_incoming_dms(debouncer: debouncer, prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [bob_to_alice_2])
XCTAssertEqual(model.dms.count, 3)
XCTAssertEqual(model.dms[0].pubkey, bob.pubkey)
let charlie_to_alice = create_dm("hi alice", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: charlie, created_at: now + 20)!
let charlie_to_alice = create_dm("hi alice", to_pk: alice.pubkey, tags: [alice.pubkey.tag], keypair: charlie, created_at: now + 20)!
handle_incoming_dms(debouncer: debouncer, prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [charlie_to_alice])
XCTAssertEqual(model.dms.count, 4)
XCTAssertEqual(model.dms[0].pubkey, charlie.pubkey)
let bob_to_alice_3 = create_dm("hi alice 3", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: bob, created_at: now + 25)!
let bob_to_alice_3 = create_dm("hi alice 3", to_pk: alice.pubkey, tags: [alice.pubkey.tag], keypair: bob, created_at: now + 25)!
handle_incoming_dms(debouncer: debouncer, prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [bob_to_alice_3])
XCTAssertEqual(model.dms.count, 4)
XCTAssertEqual(model.dms[0].pubkey, bob.pubkey)
let charlie_to_alice_2 = create_dm("hi alice 2", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: charlie, created_at: now + 30)!
let charlie_to_alice_2 = create_dm("hi alice 2", to_pk: alice.pubkey, tags: [alice.pubkey.tag], keypair: charlie, created_at: now + 30)!
handle_incoming_dms(debouncer: debouncer, prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [charlie_to_alice_2])
XCTAssertEqual(model.dms.count, 4)
XCTAssertEqual(model.dms[0].pubkey, charlie.pubkey)
}
*/
}

View File

@ -20,26 +20,31 @@ final class EventGroupViewTests: XCTestCase {
func testEventAuthorName() {
let damusState = test_damus_state()
XCTAssertEqual(event_author_name(profiles: damusState.profiles, pubkey: "pk1"), "pk1:pk1")
XCTAssertEqual(event_author_name(profiles: damusState.profiles, pubkey: "pk2"), "pk2:pk2")
XCTAssertEqual(event_author_name(profiles: damusState.profiles, pubkey: "anon"), "Anonymous")
XCTAssertEqual(event_author_name(profiles: damusState.profiles, pubkey: test_pubkey), "damus")
XCTAssertEqual(event_author_name(profiles: damusState.profiles, pubkey: test_pubkey_2), "1rppft3m:4qxhsgnj")
XCTAssertEqual(event_author_name(profiles: damusState.profiles, pubkey: ANON_PUBKEY), "Anonymous")
}
func testEventGroupUniquePubkeys() {
let damusState = test_damus_state()
let encodedPost = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}"
let pk1 = Keypair(pubkey: "pk1", privkey: nil)
let pk2 = Keypair(pubkey: "pk2", privkey: nil)
let pk3 = Keypair(pubkey: "pk3", privkey: nil)
let repost1 = NostrEvent(content: encodedPost, keypair: pk1, kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)!
let repost2 = NostrEvent(content: encodedPost, keypair: pk2, kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)!
let repost3 = NostrEvent(content: encodedPost, keypair: pk3, kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)!
let pk1 =
hex_decode_pubkey("1723a4dcc6596d84472eb74d579114d8c46b533c81a0ac76620a7605d3ff76e0")!
let pk2 =
hex_decode_pubkey("08c43696702ba1d720e4564b4ad895efdc3716b37468fb288e585368950a428a")!
let pk3 =
hex_decode_pubkey("4e563600925231e9eb35a61842c2c6c19685aa8eefdfad076d6a3f853453a299")!
let repost1 = NostrEvent(content: encodedPost, keypair: .just_pubkey(pk1), kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)!
let repost2 = NostrEvent(content: encodedPost, keypair: .just_pubkey(pk2), kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)!
let repost3 = NostrEvent(content: encodedPost, keypair: .just_pubkey(pk3), kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)!
XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: []))), [])
XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1]))), [pk1.pubkey])
XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2]))), [pk1.pubkey, pk2.pubkey])
XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2, repost3]))), [pk1.pubkey, pk2.pubkey, pk3.pubkey])
XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1]))), [pk1])
XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2]))), [pk1, pk2])
XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2, repost3]))), [pk1, pk2, pk3])
}
func testReactingToText() throws {
@ -48,18 +53,27 @@ final class EventGroupViewTests: XCTestCase {
let encodedPost = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}"
let pk1 = Keypair(pubkey: "pk1", privkey: nil)
let pk2 = Keypair(pubkey: "pk2", privkey: nil)
let pk3 = Keypair(pubkey: "pk3", privkey: nil)
let pk1_pk =
hex_decode_pubkey("938afd5f44fdf293546767dcc024b4ec09b9df422fad10d577a846f88f56c8f5")!
let pk2_pk =
hex_decode_pubkey("6b9bb7acbcdf0458a81b9e6d29bb1e23ab9b5d288e9b7fa8cee8dedc9082a466")!
let pk3_pk =
hex_decode_pubkey("b9f00c1f12b0b7a2e3960565af7aba71da9678d90faeb60bc19813f3a28840de")!
let pk1 = Keypair(pubkey: pk1_pk, privkey: nil)
let pk2 = Keypair(pubkey: pk2_pk, privkey: nil)
let pk3 = Keypair(pubkey: pk3_pk, privkey: nil)
let repost1 = NostrEvent(content: encodedPost, keypair: pk1, kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)!
let repost2 = NostrEvent(content: encodedPost, keypair: pk2, kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)!
let repost3 = NostrEvent(content: encodedPost, keypair: pk3, kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)!
XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [])), ev: test_note, pubkeys: [], locale: enUsLocale), "??")
XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_note, pubkeys: [pk1.pubkey], locale: enUsLocale), "pk1:pk1 reposted a note you were tagged in")
XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_note, pubkeys: [pk1.pubkey, pk2.pubkey], locale: enUsLocale), "pk1:pk1 and pk2:pk2 reposted a note you were tagged in")
XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost2])), ev: test_note, pubkeys: [pk1.pubkey, pk2.pubkey, pk3.pubkey], locale: enUsLocale), "pk1:pk1 and 2 others reposted a note you were tagged in")
XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_note, pubkeys: [pk1.pubkey], locale: enUsLocale), "1jw906h6:6saq3vx4 reposted a note you were tagged in")
XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_note, pubkeys: [pk1.pubkey, pk2.pubkey], locale: enUsLocale), "1jw906h6:6saq3vx4 and 1dwdm0t9:nqtnamhd reposted a note you were tagged in")
XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost2])), ev: test_note, pubkeys: [pk1.pubkey, pk2.pubkey, pk3.pubkey], locale: enUsLocale), "1jw906h6:6saq3vx4 and 2 others reposted a note you were tagged in")
Bundle.main.localizations.map { Locale(identifier: $0) }.forEach {
XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [])), ev: test_note, pubkeys: [], locale: $0), "??")

View File

@ -9,18 +9,9 @@ import XCTest
@testable import damus
final class HashtagTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testParseHashtag() {
let parsed = parse_note_content(content: "some hashtag #bitcoin derp", tags: []).blocks
let parsed = parse_note_content(content: .content("some hashtag #bitcoin derp",nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 3)
XCTAssertEqual(parsed[0].is_text, "some hashtag ")
@ -29,8 +20,8 @@ final class HashtagTests: XCTestCase {
}
func testHashtagWithComma() {
let parsed = parse_note_content(content: "some hashtag #bitcoin, cool", tags: []).blocks
let parsed = parse_note_content(content: .content("some hashtag #bitcoin, cool",nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 3)
XCTAssertEqual(parsed[0].is_text, "some hashtag ")
@ -40,7 +31,7 @@ final class HashtagTests: XCTestCase {
func testHashtagWithEmoji() {
let content = "some hashtag #bitcoin☕ cool"
let parsed = parse_note_content(content: content, tags: []).blocks
let parsed = parse_note_content(content: .content(content, nil)).blocks
let post_blocks = parse_post_blocks(content: content)
XCTAssertNotNil(parsed)
@ -57,7 +48,7 @@ final class HashtagTests: XCTestCase {
func testPowHashtag() {
let content = "pow! #ぽわ〜"
let parsed = parse_note_content(content: content, tags: []).blocks
let parsed = parse_note_content(content: .content(content,nil)).blocks
let post_blocks = parse_post_blocks(content: content)
XCTAssertNotNil(parsed)
@ -71,8 +62,8 @@ final class HashtagTests: XCTestCase {
}
func testHashtagWithAccents() {
let parsed = parse_note_content(content: "hello from #türkiye", tags: []).blocks
let parsed = parse_note_content(content: .content("hello from #türkiye",nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 2)
XCTAssertEqual(parsed[0].is_text, "hello from ")
@ -80,8 +71,8 @@ final class HashtagTests: XCTestCase {
}
func testHashtagWithNonLatinCharacters() {
let parsed = parse_note_content(content: "this is a #시험 hope it works", tags: []).blocks
let parsed = parse_note_content(content: .content("this is a #시험 hope it works",nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 3)
XCTAssertEqual(parsed[0].is_text, "this is a ")
@ -90,8 +81,8 @@ final class HashtagTests: XCTestCase {
}
func testParseHashtagEnd() {
let parsed = parse_note_content(content: "some hashtag #bitcoin", tags: []).blocks
let parsed = parse_note_content(content: .content("some hashtag #bitcoin",nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 2)
XCTAssertEqual(parsed[0].is_text, "some hashtag ")

View File

@ -20,8 +20,8 @@ final class InvoiceTests: XCTestCase {
func testParseAnyAmountInvoice() throws {
let invstr = "LNBC1P3MR5UJSP5G7SA48YD4JWTTPCHWMY4QYN4UWZQCJQ8NMWKD6QE3HCRVYTDLH9SPP57YM9TSA9NN4M4XU59XMJCXKR7YDV29DDP6LVQUT46ZW6CU3KE9GQDQ9V9H8JXQ8P3MYLZJCQPJRZJQF60PZDVNGGQWQDNERZSQN35L8CVQ3QG2Z5NSZYD0D3Q0JW2TL6VUZA7FYQQWKGQQYQQQQLGQQQQXJQQ9Q9QXPQYSGQ39EM4QJMQFKZGJXZVGL7QJMYNSWA8PGDTAGXXRG5Z92M7VLCGKQK2L2THDF8LM0AUKAURH7FVAWDLRNMVF38W4EYJDNVN9V4Z9CRS5CQCV465C"
let parsed = parse_note_content(content: invstr, tags: []).blocks
let parsed = parse_note_content(content: .content(invstr,nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 1)
XCTAssertNotNil(parsed[0].is_invoice)
@ -38,8 +38,8 @@ final class InvoiceTests: XCTestCase {
let invstr = """
LNBC1P3MR5UJSP5G7SA48YD4JWTTPCHWMY4QYN4UWZQCJQ8NMWKD6QE3HCRVYTDLH9SPP57YM9TSA9NN4M4XU59XMJCXKR7YDV29DDP6LVQUT46ZW6CU3KE9GQDQ9V9H8JXQ8P3MYLZJCQPJRZJQF60PZDVNGGQWQDNERZSQN35L8CVQ3QG2Z5NSZYD0D3Q0JW2TL6VUZA7FYQQWKGQQYQQQQLGQQQQXJQQ9Q9QXPQYSGQ39EM4QJMQFKZGJXZVGL7QJMYNSWA8PGDTAGXXRG5Z92M7VLCGKQK2L2THDF8LM0AUKAURH7FVAWDLRNMVF38W4EYJDNVN9V4Z9CRS5CQCV465C hi there
"""
let parsed = parse_note_content(content: invstr, tags: []).blocks
let parsed = parse_note_content(content: .content(invstr,nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 2)
XCTAssertNotNil(parsed[0].is_invoice)
@ -54,8 +54,8 @@ LNBC1P3MR5UJSP5G7SA48YD4JWTTPCHWMY4QYN4UWZQCJQ8NMWKD6QE3HCRVYTDLH9SPP57YM9TSA9NN
func testParseInvoiceUpper() throws {
let invstr = "LNBC100N1P357SL0SP5T9N56WDZTUN39LGDQLR30XQWKSG3K69Q4Q2RKR52APLUJW0ESN0QPP5MRQGLJK62Z20Q4NVGR6LZCYN6FHYLZCCWDVU4K77APG3ZMRKUJJQDPZW35XJUEQD9EJQCFQV3JHXCMJD9C8G6T0DCXQYJW5QCQPJRZJQT56H4GVP5YX36U2UZQA6QWCSK3E2DUUNFXPPZJ9VHYPC3WFE2WSWZ607UQQ3XQQQSQQQQQQQQQQQLQQYG9QYYSGQAGX5H20AEULJ3GDWX3KXS8U9F4MCAKDKWUAKASAMM9562FFYR9EN8YG20LG0YGNR9ZPWP68524KMDA0T5XP2WYTEX35PU8HAPYJAJXQPSQL29R"
let parsed = parse_note_content(content: invstr, tags: []).blocks
let parsed = parse_note_content(content: .content(invstr,nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 1)
XCTAssertNotNil(parsed[0].is_invoice)
@ -70,8 +70,8 @@ LNBC1P3MR5UJSP5G7SA48YD4JWTTPCHWMY4QYN4UWZQCJQ8NMWKD6QE3HCRVYTDLH9SPP57YM9TSA9NN
func testParseInvoiceWithPrefix() throws {
let invstr = "lightning:lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
let parsed = parse_note_content(content: invstr, tags: []).blocks
let parsed = parse_note_content(content: .content(invstr,nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 1)
XCTAssertNotNil(parsed[0].is_invoice)
@ -79,8 +79,8 @@ LNBC1P3MR5UJSP5G7SA48YD4JWTTPCHWMY4QYN4UWZQCJQ8NMWKD6QE3HCRVYTDLH9SPP57YM9TSA9NN
func testParseInvoiceWithPrefixCapitalized() throws {
let invstr = "LIGHTNING:LNBC100N1P357SL0SP5T9N56WDZTUN39LGDQLR30XQWKSG3K69Q4Q2RKR52APLUJW0ESN0QPP5MRQGLJK62Z20Q4NVGR6LZCYN6FHYLZCCWDVU4K77APG3ZMRKUJJQDPZW35XJUEQD9EJQCFQV3JHXCMJD9C8G6T0DCXQYJW5QCQPJRZJQT56H4GVP5YX36U2UZQA6QWCSK3E2DUUNFXPPZJ9VHYPC3WFE2WSWZ607UQQ3XQQQSQQQQQQQQQQQLQQYG9QYYSGQAGX5H20AEULJ3GDWX3KXS8U9F4MCAKDKWUAKASAMM9562FFYR9EN8YG20LG0YGNR9ZPWP68524KMDA0T5XP2WYTEX35PU8HAPYJAJXQPSQL29R"
let parsed = parse_note_content(content: invstr, tags: []).blocks
let parsed = parse_note_content(content: .content(invstr,nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 1)
XCTAssertNotNil(parsed[0].is_invoice)
@ -88,8 +88,8 @@ LNBC1P3MR5UJSP5G7SA48YD4JWTTPCHWMY4QYN4UWZQCJQ8NMWKD6QE3HCRVYTDLH9SPP57YM9TSA9NN
func testParseInvoice() throws {
let invstr = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
let parsed = parse_note_content(content: invstr, tags: []).blocks
let parsed = parse_note_content(content: .content(invstr,nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 1)
XCTAssertNotNil(parsed[0].is_invoice)

View File

@ -19,14 +19,18 @@ class LikeTests: XCTestCase {
}
func testLikeHasNotification() throws {
let liked = NostrEvent(content: "awesome #[0] post", keypair: test_keypair, tags: [["p", "cindy"], ["e", "bob"]])!
let cindy = Pubkey(hex: "9d9181f0aea6500e1f360e07b9f37e25c72169b5158ae78df53f295272b6b71c")!
let bob = Pubkey(hex: "218837fe8c94a66ae33af277bcbda45a0319e7726220cd76171b9dd1a468af91")!
let liked = NostrEvent(content: "awesome #[0] post",
keypair: test_keypair,
tags: [cindy.tag, bob.tag])!
let id = liked.id
let like_ev = make_like_event(keypair: test_keypair_full, liked: liked)!
XCTAssertTrue(like_ev.references(id: test_keypair.pubkey, key: "p"))
XCTAssertTrue(like_ev.references(id: "cindy", key: "p"))
XCTAssertTrue(like_ev.references(id: "bob", key: "e"))
XCTAssertEqual(like_ev.last_refid()!.ref_id, id)
XCTAssertTrue(like_ev.referenced_pubkeys.contains(test_keypair.pubkey))
XCTAssertTrue(like_ev.referenced_pubkeys.contains(cindy))
XCTAssertTrue(like_ev.referenced_pubkeys.contains(bob))
XCTAssertEqual(like_ev.last_refid()!, id)
}
func testToReactionEmoji() {

View File

@ -19,53 +19,53 @@ final class ListTests: XCTestCase {
}
func testCreateMuteList() throws {
let privkey = "87f313b03f2548e6eaf1c188db47078e08e894252949779b639b28db0891937a"
let pubkey = "4b0c29bf96496130c1253102f6870c0eee05db38a257315858272aa43fd19685"
let to_mute = "2fa2630fea3d2c188c49f2799fcd92f0e9879ea6a36ae60770a5428ed6c19edd"
let privkey = test_keypair_full.privkey
let pubkey = test_keypair_full.pubkey
let to_mute = test_pubkey
let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: to_mute)!
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(to_mute))!
XCTAssertEqual(mutelist.pubkey, pubkey)
XCTAssertEqual(mutelist.content, "")
XCTAssertEqual(mutelist.tags.count, 2)
XCTAssertEqual(mutelist.tags[0][0], "d")
XCTAssertEqual(mutelist.tags[0][1], "mute")
XCTAssertEqual(mutelist.tags[1][0], "p")
XCTAssertEqual(mutelist.tags[1][1], to_mute)
XCTAssertEqual(mutelist.tags[0][0].string(), "d")
XCTAssertEqual(mutelist.tags[0][1].string(), "mute")
XCTAssertEqual(mutelist.tags[1][0].string(), "p")
XCTAssertEqual(mutelist.tags[1][1].string(), to_mute.hex())
}
func testCreateAndRemoveMuteList() throws {
let privkey = "87f313b03f2548e6eaf1c188db47078e08e894252949779b639b28db0891937a"
let pubkey = "4b0c29bf96496130c1253102f6870c0eee05db38a257315858272aa43fd19685"
let to_mute = "2fa2630fea3d2c188c49f2799fcd92f0e9879ea6a36ae60770a5428ed6c19edd"
let privkey = test_keypair_full.privkey
let pubkey = test_keypair_full.pubkey
let to_mute = test_pubkey
let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: to_mute)!
let new = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: to_mute)!
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(to_mute))!
let new = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .pubkey(to_mute))!
XCTAssertEqual(new.pubkey, pubkey)
XCTAssertEqual(new.content, "")
XCTAssertEqual(new.tags.count, 1)
XCTAssertEqual(new.tags[0][0], "d")
XCTAssertEqual(new.tags[0][1], "mute")
XCTAssertEqual(new.tags[0][0].string(), "d")
XCTAssertEqual(new.tags[0][1].string(), "mute")
}
func testAddToExistingMutelist() throws {
let privkey = "87f313b03f2548e6eaf1c188db47078e08e894252949779b639b28db0891937a"
let pubkey = "4b0c29bf96496130c1253102f6870c0eee05db38a257315858272aa43fd19685"
let to_mute = "2fa2630fea3d2c188c49f2799fcd92f0e9879ea6a36ae60770a5428ed6c19edd"
let to_mute_2 = "976b4ab41f8634119b4f21f57ef5836a4bef65d0bf72c7ced67b8b170ba4a38d"
let privkey = test_keypair_full.privkey
let pubkey = test_keypair_full.pubkey
let to_mute = test_pubkey
let to_mute_2 = test_pubkey_2
let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: to_mute)!
let new = create_or_update_mutelist(keypair: keypair, mprev: mutelist, to_add: to_mute_2)!
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(to_mute))!
let new = create_or_update_mutelist(keypair: keypair, mprev: mutelist, to_add: .pubkey(to_mute_2))!
XCTAssertEqual(new.pubkey, pubkey)
XCTAssertEqual(new.content, "")
XCTAssertEqual(new.tags.count, 3)
XCTAssertEqual(new.tags[0][0], "d")
XCTAssertEqual(new.tags[0][1], "mute")
XCTAssertEqual(new.tags[1][0], "p")
XCTAssertEqual(new.tags[1][1], to_mute)
XCTAssertEqual(new.tags[2][0], "p")
XCTAssertEqual(new.tags[2][1], to_mute_2)
XCTAssertEqual(new.tags[0][0].string(), "d")
XCTAssertEqual(new.tags[0][1].string(), "mute")
XCTAssertEqual(new.tags[1][0].string(), "p")
XCTAssertEqual(new.tags[1][1].string(), to_mute.hex())
XCTAssertEqual(new.tags[2][0].string(), "p")
XCTAssertEqual(new.tags[2][1].string(), to_mute_2.hex())
}
}

View File

@ -24,13 +24,14 @@ class ContentParserTests: XCTestCase {
let url = "https://media.tenor.com/5MibLt95scAAAAAC/%ED%98%BC%ED%8C%8C%EB%A7%9D-%ED%94%BC%EC%9E%90.gif"
let content = "gm 🤙\(url)"
let blocks = parse_note_content(content: content, tags: []).blocks
let blocks = parse_note_content(content: .content(content,nil)).blocks
XCTAssertEqual(blocks.count, 2)
XCTAssertEqual(blocks[0], .text("gm 🤙"))
XCTAssertEqual(blocks[1], .url(URL(string: url)!))
}
/*
func test_damus_parse_content_can_parse_mention_without_white_space_at_front() throws {
var bs = note_blocks()
bs.num_blocks = 0;
@ -38,7 +39,7 @@ class ContentParserTests: XCTestCase {
blocks_init(&bs)
let content = "#[0], #[1],#[2],#[3]#[4],#[5],#[6],#[7], #[8], \n#[9], #[10], #[11], #[12]"
let tagsString = "[[\"p\",\"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2\"],[\"p\",\"0339bb0d9d818ba126a39385a5edee5651993af7c21f18d4ceb0ba8c9de0d463\"],[\"p\",\"e7424ad457e512fdf4764a56bf6d428a06a13a1006af1fb8e0fe32f6d03265c7\"],[\"p\",\"520830c334a3f79f88cac934580d26f91a7832c6b21fb9625690ea2ed81b5626\"],[\"p\",\"971615b70ad9ec896f8d5ba0f2d01652f1dfe5f9ced81ac9469ca7facefad68b\"],[\"p\",\"2779f3d9f42c7dee17f0e6bcdcf89a8f9d592d19e3b1bbd27ef1cffd1a7f98d1\"],[\"p\",\"17538dc2a62769d09443f18c37cbe358fab5bbf981173542aa7c5ff171ed77c4\"],[\"p\",\"985a7c6b0e75508ad74c4110b2e52dfba6ce26063d80bca218564bd083a72b99\"],[\"p\",\"7fb2a29bd1a41d9a8ca43a19a7dcf3a8522f1bc09b4086253539190e9c29c51a\"],[\"p\",\"b88c7f007bbf3bc2fcaeff9e513f186bab33782c0baa6a6cc12add78b9110ba3\"],[\"p\",\"2f4fa408d85b962d1fe717daae148a4c98424ab2e10c7dd11927e101ed3257b2\"],[\"p\",\"bd1e19980e2c91e6dc657e92c25762ca882eb9272d2579e221f037f93788de91\"],[\"p\",\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\"]]"
let tags = try decoder.decode([[String]].self, from: tagsString.data(using: .utf8)!)
@ -72,4 +73,5 @@ class ContentParserTests: XCTestCase {
i += 1
}
}
*/
}

View File

@ -18,37 +18,37 @@ final class NIP19Tests: XCTestCase {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
/*
func test_parse_nprofile() throws {
let res = parse_note_content(content: "nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p", tags: []).blocks
let res = parse_note_content(content: .content("nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p")).blocks
XCTAssertEqual(res.count, 1)
let expected_ref = ReferencedId(ref_id: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", relay_id: "wss://r.x.com", key: "p")
let expected_mention = Mention(index: nil, type: .pubkey, ref: expected_ref)
XCTAssertEqual(res[0], .mention(expected_mention))
}
*/
func test_parse_npub() throws {
let res = parse_note_content(content: "nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg ", tags: []).blocks
let res = parse_note_content(content: .content("nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg ",nil)).blocks
XCTAssertEqual(res.count, 2)
let expected_ref = ReferencedId(ref_id: "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e", relay_id: nil, key: "p")
let expected_mention = Mention(index: nil, type: .pubkey, ref: expected_ref)
let expected_ref = Pubkey(hex: "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e")!
let expected_mention: Mention<MentionRef> = Mention(index: nil, ref: .pubkey(expected_ref))
XCTAssertEqual(res[0], .mention(expected_mention))
}
func test_parse_note() throws {
let res = parse_note_content(content: " nostr:note1s4p70596lv50x0zftuses32t6ck8x6wgd4edwacyetfxwns2jtysux7vep", tags: []).blocks
let res = parse_note_content(content: .content(" nostr:note1s4p70596lv50x0zftuses32t6ck8x6wgd4edwacyetfxwns2jtysux7vep",nil)).blocks
XCTAssertEqual(res.count, 2)
let expected_ref = ReferencedId(ref_id: "8543e7d0bafb28f33c495f2198454bd62c7369c86d72d77704cad2674e0a92c9", relay_id: nil, key: "e")
let expected_mention = Mention(index: nil, type: .event, ref: expected_ref)
XCTAssertEqual(res[1], .mention(expected_mention))
let note_id = NoteId(hex:"8543e7d0bafb28f33c495f2198454bd62c7369c86d72d77704cad2674e0a92c9")!
XCTAssertEqual(res[1], .mention(.any(.note(note_id))))
}
func test_mention_with_adjacent() throws {
let res = parse_note_content(content: " nostr:note1s4p70596lv50x0zftuses32t6ck8x6wgd4edwacyetfxwns2jtysux7vep?", tags: []).blocks
let res = parse_note_content(content: .content(" nostr:note1s4p70596lv50x0zftuses32t6ck8x6wgd4edwacyetfxwns2jtysux7vep?",nil)).blocks
XCTAssertEqual(res.count, 3)
let expected_ref = ReferencedId(ref_id: "8543e7d0bafb28f33c495f2198454bd62c7369c86d72d77704cad2674e0a92c9", relay_id: nil, key: "e")
let expected_mention = Mention(index: nil, type: .event, ref: expected_ref)
let note_id = NoteId(hex: "8543e7d0bafb28f33c495f2198454bd62c7369c86d72d77704cad2674e0a92c9")!
XCTAssertEqual(res[0], .text(" "))
XCTAssertEqual(res[1], .mention(expected_mention))
XCTAssertEqual(res[1], .mention(.any(.note(note_id))))
XCTAssertEqual(res[2], .text("?"))
}

View File

@ -39,7 +39,7 @@ final class NostrScriptTests: XCTestCase {
let data = try load_bool_set_test_wasm().bytes
let pool = RelayPool()
let script = NostrScript(pool: pool, data: data)
let pk = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
let pk = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
UserSettingsStore.pubkey = pk
let key = pk_setting_key(pk, key: "nozaps")
UserDefaults.standard.set(true, forKey: key)
@ -103,7 +103,7 @@ final class NostrScriptTests: XCTestCase {
pool.connect(to: ["wss://cache0.primal.net/cache17"])
self.wait(for: [resume_expected], timeout: 10.0)
self.wait(for: [resume_expected], timeout: 5.0)
}
*/

View File

@ -10,8 +10,10 @@ import XCTest
class NoteContentViewTests: XCTestCase {
func testRenderBlocksWithNonLatinHashtags() {
let parsed: Blocks = parse_note_content(content: "Damusはかっこいいです #cool #かっこいい", tags: [["t", "かっこいい"]])
let content = "Damusはかっこいいです #cool #かっこいい"
let note = NostrEvent(content: content, keypair: test_keypair, tags: [["t", "かっこいい"]])!
let parsed: Blocks = parse_note_content(content: .init(note: note, privkey: test_keypair.privkey))
let testState = test_damus_state()
let text: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles)

View File

@ -32,8 +32,8 @@ class ProfileDatabaseTests: XCTestCase {
}
func testStoreAndRetrieveProfile() async throws {
let id = "test-id"
let id = test_pubkey
let profile = test_profile
// make sure it's not there yet
@ -58,7 +58,7 @@ class ProfileDatabaseTests: XCTestCase {
}
func testRejectOutdatedProfile() async throws {
let id = "test-id"
let id = test_pubkey
// store a profile
let profile = test_profile
@ -80,8 +80,8 @@ class ProfileDatabaseTests: XCTestCase {
}
func testUpdateExistingProfile() async throws {
let id = "test-id"
let id = test_pubkey
// store a profile
let profile = test_profile
let profile_last_update = Date.now
@ -102,7 +102,7 @@ class ProfileDatabaseTests: XCTestCase {
XCTAssertEqual(database.count, 0)
// store a profile
let id = "test-id"
let id = test_pubkey
let profile = test_profile
let profile_last_update = Date.now
try await database.upsert(id: id, profile: profile, last_update: profile_last_update)
@ -110,7 +110,7 @@ class ProfileDatabaseTests: XCTestCase {
XCTAssertEqual(database.count, 1)
// store another profile
let id2 = "test-id-2"
let id2 = test_pubkey_2
let profile2 = test_profile
let profile_last_update2 = Date.now
try await database.upsert(id: id2, profile: profile2, last_update: profile_last_update2)

View File

@ -23,13 +23,19 @@ final class ProfileViewTests: XCTestCase {
func testFollowedByString() throws {
let profiles = test_damus_state().profiles
XCTAssertEqual(followedByString(["pk1"], profiles: profiles, locale: enUsLocale), "Followed by pk1:pk1")
XCTAssertEqual(followedByString(["pk1", "pk2"], profiles: profiles, locale: enUsLocale), "Followed by pk1:pk1 & pk2:pk2")
XCTAssertEqual(followedByString(["pk1", "pk2", "pk3"], profiles: profiles, locale: enUsLocale), "Followed by pk1:pk1, pk2:pk2 & pk3:pk3")
XCTAssertEqual(followedByString(["pk1", "pk2", "pk3", "pk4",], profiles: profiles, locale: enUsLocale), "Followed by pk1:pk1, pk2:pk2, pk3:pk3 & 1 other")
XCTAssertEqual(followedByString(["pk1", "pk2", "pk3", "pk4", "pk5"], profiles: profiles, locale: enUsLocale), "Followed by pk1:pk1, pk2:pk2, pk3:pk3 & 2 others")
let pk1 = test_pubkey
let pk2 = test_pubkey_2
let pk3 = Pubkey(hex: "b42e44b555013239a0d5dcdb09ebde0857cd8a5a57efbba5a2b6ac78833cb9f0")!
let pk4 = Pubkey(hex: "cc590e46363d0fa66bb27081368d01f169b8ffc7c614629d4e9eef6c88b38670")!
let pk5 = Pubkey(hex: "f2aa579bb998627e04a8f553842a09446360c9d708c6141dd119c479f6ab9d29")!
let pubkeys = ["pk1", "pk2", "pk3", "pk4", "pk5", "pk6", "pk7", "pk8", "pk9", "pk10"]
XCTAssertEqual(followedByString([pk1], profiles: profiles, locale: enUsLocale), "Followed by damus")
XCTAssertEqual(followedByString([pk1, pk2], profiles: profiles, locale: enUsLocale), "Followed by damus & 1rppft3m:4qxhsgnj")
XCTAssertEqual(followedByString([pk1, pk2, pk3], profiles: profiles, locale: enUsLocale), "Followed by damus, 1rppft3m:4qxhsgnj & 1kshyfd2:cq04aze0")
XCTAssertEqual(followedByString([pk1, pk2, pk3, pk4,], profiles: profiles, locale: enUsLocale), "Followed by damus, 1rppft3m:4qxhsgnj, 1kshyfd2:cq04aze0 & 1 other")
XCTAssertEqual(followedByString([pk1, pk2, pk3, pk4, pk5], profiles: profiles, locale: enUsLocale), "Followed by damus, 1rppft3m:4qxhsgnj, 1kshyfd2:cq04aze0 & 2 others")
let pubkeys = [pk1, pk2, pk3, pk4, pk5, pk1, pk2, pk3, pk4, pk5]
Bundle.main.localizations.map { Locale(identifier: $0) }.forEach {
for count in 1...10 {
XCTAssertNoThrow(followedByString(pubkeys.prefix(count).map { $0 }, profiles: profiles, locale: $0))

View File

@ -19,11 +19,13 @@ class ReplyTests: XCTestCase {
}
func testMentionIsntReply() throws {
let evid = NoteId(hex: "4090a9017a2beac3f17795d1aafb80d9f2b9eda97e4738501082ed5c927be014")!
let content = "this is #[0] a mention"
let tags = [["e", "event_id"]]
let blocks = parse_note_content(content: content, tags: tags).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: tags)
let tags = [evid.tag]
let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)!
let blocks = parse_note_content(content: .init(note: ev, privkey: test_keypair.privkey)).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags)
XCTAssertEqual(event_refs.count, 1)
let ref = event_refs[0]
@ -31,14 +33,13 @@ class ReplyTests: XCTestCase {
XCTAssertNil(ref.is_reply)
XCTAssertNil(ref.is_thread_id)
XCTAssertNil(ref.is_direct_reply)
XCTAssertEqual(ref.is_mention?.type, .event)
XCTAssertEqual(ref.is_mention?.ref.ref_id, "event_id")
XCTAssertEqual(ref.is_mention, .some(.init(note_id: evid)))
}
func testUrlAnchorsAreNotHashtags() {
let content = "this is my link: https://jb55.com/index.html#buybitcoin this is not a hashtag!"
let blocks = parse_post_blocks(content: content)
XCTAssertEqual(blocks.count, 3)
XCTAssertEqual(blocks[0].is_text, "this is my link: ")
XCTAssertEqual(blocks[1].is_url, URL(string: "https://jb55.com/index.html#buybitcoin")!)
@ -93,30 +94,32 @@ class ReplyTests: XCTestCase {
func testRootReplyWithMention() throws {
let content = "this is #[1] a mention"
let tags = [["e", "thread_id"], ["e", "mentioned_id"]]
let blocks = parse_note_content(content: content, tags: tags).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: tags)
let thread_id = NoteId(hex: "c75e5cbafbefd5de2275f831c2a2386ea05ec5e5a78a5ccf60d467582db48945")!
let mentioned_id = NoteId(hex: "5a534797e8cd3b9f4c1cf63e20e48bd0e8bd7f8c4d6353fbd576df000f6f54d3")!
let tags = [thread_id.tag, mentioned_id.tag]
let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)!
let blocks = parse_note_content(content: .init(note: ev, privkey: test_keypair.privkey)).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags)
XCTAssertEqual(event_refs.count, 2)
XCTAssertNotNil(event_refs[0].is_reply)
XCTAssertNotNil(event_refs[0].is_thread_id)
XCTAssertNotNil(event_refs[0].is_reply)
XCTAssertNotNil(event_refs[0].is_direct_reply)
XCTAssertEqual(event_refs[0].is_reply?.ref_id, "thread_id")
XCTAssertEqual(event_refs[0].is_thread_id?.ref_id, "thread_id")
XCTAssertEqual(event_refs[0].is_reply, .some(NoteRef(note_id: thread_id)))
XCTAssertEqual(event_refs[0].is_thread_id, .some(NoteRef(note_id: thread_id)))
XCTAssertNotNil(event_refs[1].is_mention)
XCTAssertEqual(event_refs[1].is_mention?.type, .event)
XCTAssertEqual(event_refs[1].is_mention?.ref.ref_id, "mentioned_id")
XCTAssertEqual(event_refs[1].is_mention, .some(NoteRef(note_id: mentioned_id)))
}
func testEmptyMention() throws {
let content = "this is some & content"
let tags: [[String]] = []
let blocks = parse_note_content(content: content, tags: tags).blocks
let ev = NostrEvent(content: content, keypair: test_keypair, tags: [])!
let blocks = parse_note_content(content: .init(note: ev, privkey: test_keypair.privkey)).blocks
let post_blocks = parse_post_blocks(content: content)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
let event_refs = interpret_event_refs(blocks: blocks, tags: tags)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: [])
let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags)
XCTAssertEqual(event_refs.count, 0)
XCTAssertEqual(post_tags.blocks.count, 1)
XCTAssertEqual(post_tags.tags.count, 0)
@ -126,16 +129,15 @@ class ReplyTests: XCTestCase {
func testManyMentions() throws {
let content = "#[10]"
let tags: [[String]] = [[],[],[],[],[],[],[],[],[],[],["p", "3e999f94e2cb34ef44a64b351141ac4e51b5121b2d31aed4a6c84602a1144692"]]
let blocks = parse_note_content(content: content, tags: tags).blocks
let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)!
let blocks = parse_note_content(content: .init(note: ev, privkey: test_keypair.privkey)).blocks
let mentions = blocks.filter { $0.is_mention != nil }
XCTAssertEqual(mentions.count, 1)
}
func testNewlineMentions() throws {
let pk = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"
guard let hex_pk = bech32_pubkey_decode(pk) else {
return
}
let bech32_pk = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"
let pk = bech32_pubkey_decode(bech32_pk)!
let profile = Profile(name: "jb55")
let post = user_tag_attr_string(profile: profile, pubkey: pk)
@ -143,50 +145,55 @@ class ReplyTests: XCTestCase {
post.append(user_tag_attr_string(profile: profile, pubkey: pk))
post.append(.init(string: "\n"))
let post_note = build_post(post: post, action: .posting(.none), uploadedMedias: [], references: [.p(hex_pk)])
let post_note = build_post(post: post, action: .posting(.none), uploadedMedias: [], references: [.pubkey(pk)])
let expected_render = "nostr:\(pk)\nnostr:\(pk)"
let expected_render = "nostr:\(pk.npub)\nnostr:\(pk.npub)"
XCTAssertEqual(post_note.content, expected_render)
let blocks = parse_note_content(content: post_note.content, tags: []).blocks
let blocks = parse_note_content(content: .content(post_note.content,nil)).blocks
let rendered = render_blocks(blocks: blocks)
XCTAssertEqual(rendered, expected_render)
XCTAssertEqual(blocks.count, 3)
XCTAssertEqual(blocks[0].is_mention, .pubkey(hex_pk))
XCTAssertEqual(blocks[0].is_mention, .any(.pubkey(pk)))
XCTAssertEqual(blocks[1].is_text, "\n")
XCTAssertEqual(blocks[2].is_mention, .pubkey(hex_pk))
XCTAssertEqual(blocks[2].is_mention, .any(.pubkey(pk)))
}
func testThreadedReply() throws {
let content = "this is some content"
let tags = [["e", "thread_id"], ["e", "reply_id"]]
let blocks = parse_note_content(content: content, tags: tags).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: tags)
let thread_id = NoteId(hex: "da256fb52146dc565c6c6b9ef906117c665864dc02b14a7b853eca244729c2f2")!
let reply_id = NoteId(hex: "80093e9bdb495728f54cda2bad4aed096877189552b3d41264e73b9a9595be22")!
let tags = [thread_id.tag, reply_id.tag]
let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)!
let blocks = parse_note_content(content: .init(note: ev, privkey: test_keypair.privkey)).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags)
XCTAssertEqual(event_refs.count, 2)
let r1 = event_refs[0]
let r2 = event_refs[1]
XCTAssertEqual(r1.is_thread_id!.ref_id, "thread_id")
XCTAssertEqual(r2.is_reply!.ref_id, "reply_id")
XCTAssertEqual(r2.is_direct_reply!.ref_id, "reply_id")
XCTAssertEqual(r1.is_thread_id, .some(.note_id(thread_id)))
XCTAssertEqual(r2.is_reply, .some(.note_id(reply_id)))
XCTAssertEqual(r2.is_direct_reply, .some(.note_id(reply_id)))
XCTAssertNil(r1.is_direct_reply)
}
func testRootReply() throws {
let content = "this is a reply"
let tags = [["e", "thread_id"]]
let blocks = parse_note_content(content: content, tags: tags).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: tags)
let thread_id = NoteId(hex: "53f60f5114c06f069ffe9da2bc033e533d09cae44d37a8462154a663771a4ce6")!
let tags = [thread_id.tag]
let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)!
let blocks = parse_note_content(content: .content(ev.content,nil)).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags)
XCTAssertEqual(event_refs.count, 1)
let r = event_refs[0]
XCTAssertEqual(r.is_direct_reply!.ref_id, "thread_id")
XCTAssertEqual(r.is_reply!.ref_id, "thread_id")
XCTAssertEqual(r.is_thread_id!.ref_id, "thread_id")
XCTAssertEqual(r.is_direct_reply, .some(.note_id(thread_id)))
XCTAssertEqual(r.is_reply, .some(.note_id(thread_id)))
XCTAssertEqual(r.is_thread_id, .some(.note_id(thread_id)))
XCTAssertNil(r.is_mention)
}
@ -194,13 +201,13 @@ class ReplyTests: XCTestCase {
let content = "cc@jb55"
let profile = Profile(name: "jb55")
let tag = user_tag_attr_string(profile: profile, pubkey: "pk")
let tag = user_tag_attr_string(profile: profile, pubkey: test_pubkey)
let appended = append_user_tag(tag: tag, post: .init(string: content), word_range: .init(2...6))
let new_post = appended.post
try new_post.testAttributes(conditions: [
{ let link = $0[.link] as? String; XCTAssertNil(link) },
{ let link = $0[.link] as! String; XCTAssertEqual(link, "damus:nostr:pk") },
{ let link = $0[.link] as! String; XCTAssertEqual(link, "damus:nostr:\(test_pubkey.npub)") },
{ let link = $0[.link] as? String; XCTAssertNil(link) }
])
@ -211,13 +218,13 @@ class ReplyTests: XCTestCase {
let content = "😎@jb55"
let profile = Profile(name: "jb55")
let tag = user_tag_attr_string(profile: profile, pubkey: "pk")
let tag = user_tag_attr_string(profile: profile, pubkey: test_pubkey)
let appended = append_user_tag(tag: tag, post: .init(string: content), word_range: .init(2...6))
let new_post = appended.post
try new_post.testAttributes(conditions: [
{ let link = $0[.link] as? String; XCTAssertNil(link) },
{ let link = $0[.link] as! String; XCTAssertEqual(link, "damus:nostr:pk") },
{ let link = $0[.link] as! String; XCTAssertEqual(link, "damus:nostr:\(test_pubkey.npub)") },
{ let link = $0[.link] as? String; XCTAssertNil(link) }
])
@ -231,13 +238,13 @@ class ReplyTests: XCTestCase {
"""
let profile = Profile(name: "jb55")
let tag = user_tag_attr_string(profile: profile, pubkey: "pk")
let tag = user_tag_attr_string(profile: profile, pubkey: test_pubkey)
let appended = append_user_tag(tag: tag, post: .init(string: content), word_range: .init(1...5))
let new_post = appended.post
try new_post.testAttributes(conditions: [
{ let link = $0[.link] as? String; XCTAssertNil(link) },
{ let link = $0[.link] as! String; XCTAssertEqual(link, "damus:nostr:pk") },
{ let link = $0[.link] as! String; XCTAssertEqual(link, "damus:nostr:\(test_pubkey.npub)") },
{ let link = $0[.link] as? String; XCTAssertNil(link) },
])
@ -248,12 +255,12 @@ class ReplyTests: XCTestCase {
let content = "@jb55"
let profile = Profile(name: "jb55")
let tag = user_tag_attr_string(profile: profile, pubkey: "pk")
let tag = user_tag_attr_string(profile: profile, pubkey: test_pubkey)
let appended = append_user_tag(tag: tag, post: .init(string: content), word_range: .init(0...4))
let new_post = appended.post
try new_post.testAttributes(conditions: [
{ let link = $0[.link] as! String; XCTAssertEqual(link, "damus:nostr:pk") },
{ let link = $0[.link] as! String; XCTAssertEqual(link, "damus:nostr:\(test_pubkey.npub)") },
{ let link = $0[.link] as? String; XCTAssertNil(link) },
])
@ -264,13 +271,13 @@ class ReplyTests: XCTestCase {
let content = "cc @jb55"
let profile = Profile(name: "jb55")
let tag = user_tag_attr_string(profile: profile, pubkey: "pk")
let tag = user_tag_attr_string(profile: profile, pubkey: test_pubkey)
let appended = append_user_tag(tag: tag, post: .init(string: content), word_range: .init(3...7))
let new_post = appended.post
try new_post.testAttributes(conditions: [
{ let link = $0[.link] as? String; XCTAssertNil(link) },
{ let link = $0[.link] as! String; XCTAssertEqual(link, "damus:nostr:pk") },
{ let link = $0[.link] as! String; XCTAssertEqual(link, "damus:nostr:\(test_pubkey.npub)") },
{ let link = $0[.link] as? String; XCTAssertNil(link) }
])
@ -279,15 +286,19 @@ class ReplyTests: XCTestCase {
func testNoReply() throws {
let content = "this is a #[0] reply"
let blocks = parse_note_content(content: content, tags: []).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: [])
let ev = NostrEvent(content: content, keypair: test_keypair, tags: [])!
let blocks = parse_note_content(content: .init(note: ev, privkey: test_keypair.privkey)).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags:ev.tags)
XCTAssertEqual(event_refs.count, 0)
}
func testParseMention() throws {
let parsed = parse_note_content(content: "this is #[0] a mention", tags: [["e", "event_id"]]).blocks
let note_id = NoteId(hex: "53f60f5114c06f069ffe9da2bc033e533d09cae44d37a8462154a663771a4ce6")!
let tags = [note_id.tag]
let ev = NostrEvent(content: "this is #[0] a mention", keypair: test_keypair, tags: tags)!
let parsed = parse_note_content(content: .init(note: ev, privkey: test_keypair.privkey)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 3)
XCTAssertEqual(parsed[0].is_text, "this is ")
@ -301,76 +312,71 @@ class ReplyTests: XCTestCase {
}
func testBech32MentionAtStart() throws {
let pk = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"
let hex_pk = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
let content = "@\(pk) hello there"
let pk = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
let content = "@\(pk.npub) hello there"
let blocks = parse_post_blocks(content: content)
XCTAssertEqual(blocks.count, 2)
XCTAssertEqual(blocks[0].is_mention, .pubkey(hex_pk))
XCTAssertEqual(blocks[0].is_mention, .any(.pubkey(pk)))
XCTAssertEqual(blocks[1].is_text, " hello there")
}
func testBech32MentionAtEnd() throws {
let pk = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"
let hex_pk = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
let content = "this is a @\(pk)"
let pk = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
let content = "this is a @\(pk.npub)"
let blocks = parse_post_blocks(content: content)
XCTAssertEqual(blocks.count, 2)
XCTAssertEqual(blocks[1].is_mention, .pubkey(hex_pk))
XCTAssertEqual(blocks[1].is_mention, .any(.pubkey(pk)))
XCTAssertEqual(blocks[0].is_text, "this is a ")
}
func testNpubMention() throws {
let evid = "0000000000000000000000000000000000000000000000000000000000000005"
let pk = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"
let hex_pk = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
let content = "this is a @\(pk) mention"
let reply_ref = ReferencedId(ref_id: evid, relay_id: nil, key: "e")
let evid = NoteId(hex: "71ba3e5ddaf48103be294aa370e470fb60b6c8bca3fb01706eecd00054c2f588")!
let pk = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
let content = "this is a @\(pk.npub) mention"
let blocks = parse_post_blocks(content: content)
let post = NostrPost(content: content, references: [reply_ref])
let post = NostrPost(content: content, references: [.event(evid)])
let ev = post_to_event(post: post, keypair: test_keypair_full)!
XCTAssertEqual(ev.tags.count, 2)
XCTAssertEqual(blocks.count, 3)
XCTAssertEqual(blocks[1].is_mention, .pubkey(hex_pk))
XCTAssertEqual(blocks[1].is_mention, .any(.pubkey(pk)))
XCTAssertEqual(ev.content, "this is a nostr:npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s mention")
}
func testNsecMention() throws {
let evid = "0000000000000000000000000000000000000000000000000000000000000005"
let pk = "nsec1jmzdz7d0ldqctdxwm5fzue277ttng2pk28n2u8wntc2r4a0w96ssnyukg7"
let hex_pk = "ccf95d668650178defca5ac503693b6668eb77895f610178ff8ed9fe5cf9482e"
let content = "this is a @\(pk) mention"
let reply_ref = ReferencedId(ref_id: evid, relay_id: nil, key: "e")
let evid = NoteId(hex: "71ba3e5ddaf48103be294aa370e470fb60b6c8bca3fb01706eecd00054c2f588")!
let pk = Pubkey(hex: "ccf95d668650178defca5ac503693b6668eb77895f610178ff8ed9fe5cf9482e")!
let nsec = "nsec1jmzdz7d0ldqctdxwm5fzue277ttng2pk28n2u8wntc2r4a0w96ssnyukg7"
let content = "this is a @\(nsec) mention"
let blocks = parse_post_blocks(content: content)
let post = NostrPost(content: content, references: [reply_ref])
let post = NostrPost(content: content, references: [.event(evid)])
let ev = post_to_event(post: post, keypair: test_keypair_full)!
XCTAssertEqual(ev.tags.count, 2)
XCTAssertEqual(blocks.count, 3)
XCTAssertEqual(blocks[1].is_mention, .pubkey(hex_pk))
XCTAssertEqual(blocks[1].is_mention, .any(.pubkey(pk)))
XCTAssertEqual(ev.content, "this is a nostr:npub1enu46e5x2qtcmm72ttzsx6fmve5wkauftassz78l3mvluh8efqhqejf3v4 mention")
}
func testReplyMentions() throws {
let privkey = "0fc2092231f958f8d57d66f5e238bb45b6a2571f44c0ce024bbc6f3a9c8a15fe"
let pubkey = "30c6d1dc7f7c156794fa15055e651b758a61b99f50fcf759de59386050bf6ae2"
let npub = "npub1xrrdrhrl0s2k0986z5z4uegmwk9xrwvl2r70wkw7tyuxq59ldt3qh09eay"
let pubkey = Pubkey(hex: "30c6d1dc7f7c156794fa15055e651b758a61b99f50fcf759de59386050bf6ae2")!
let thread_id = NoteId(hex: "a250fc93570c3e87f9c9b08d6b3ef7b8e05d346df8a52c69e30ffecdb178fb9e")!
let reply_id = NoteId(hex: "9a180a10f16dac9566543ad1fc29616aab272b0cf123ab5d58843e16f4ef03a3")!
let refs = [
ReferencedId(ref_id: "thread_id", relay_id: nil, key: "e"),
ReferencedId(ref_id: "reply_id", relay_id: nil, key: "e"),
ReferencedId(ref_id: pubkey, relay_id: nil, key: "p"),
let refs: [RefId] = [
.event(thread_id),
.event(reply_id),
.pubkey(pubkey)
]
let post = NostrPost(content: "this is a (@\(npub)) mention", references: refs)
let post = NostrPost(content: "this is a (@\(pubkey.npub)) mention", references: refs)
let ev = post_to_event(post: post, keypair: test_keypair_full)!
XCTAssertEqual(ev.content, "this is a (nostr:\(npub)) mention")
XCTAssertEqual(ev.tags[2][1], pubkey)
XCTAssertEqual(ev.content, "this is a (nostr:\(pubkey.npub)) mention")
XCTAssertEqual(ev.tags[2][1].string(), pubkey.description)
}
func testInvalidPostReference() throws {
@ -413,14 +419,13 @@ class ReplyTests: XCTestCase {
}
func testParsePostUriPubkeyReference() throws {
let id = "6fec2ee6cfff779fe8560976b3d9df782b74577f0caefa7a77c0ed4c3749b5de"
let npub = try XCTUnwrap(bech32_pubkey(id))
let parsed = parse_post_blocks(content: "this is a nostr:\(npub) event mention")
let id = Pubkey(hex: "6fec2ee6cfff779fe8560976b3d9df782b74577f0caefa7a77c0ed4c3749b5de")!
let parsed = parse_post_blocks(content: "this is a nostr:\(id.npub) event mention")
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 3)
XCTAssertEqual(parsed[0].is_text, "this is a ")
XCTAssertEqual(parsed[1].is_mention, .pubkey(id))
XCTAssertEqual(parsed[1].is_mention, .any(.pubkey(id)))
XCTAssertEqual(parsed[2].is_text, " event mention")
guard case .text(let t1) = parsed[0] else {
@ -437,14 +442,13 @@ class ReplyTests: XCTestCase {
}
func testParsePostUriReference() throws {
let id = "6fec2ee6cfff779fe8560976b3d9df782b74577f0caefa7a77c0ed4c3749b5de"
let note_id = try XCTUnwrap(bech32_note_id(id))
let parsed = parse_post_blocks(content: "this is a nostr:\(note_id) event mention")
let id = NoteId(hex: "6fec2ee6cfff779fe8560976b3d9df782b74577f0caefa7a77c0ed4c3749b5de")!
let parsed = parse_post_blocks(content: "this is a nostr:\(id.bech32) event mention")
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 3)
XCTAssertEqual(parsed[0].is_text, "this is a ")
XCTAssertEqual(parsed[1].is_mention, .note(id))
XCTAssertEqual(parsed[1].is_mention, .any(.note(id)))
XCTAssertEqual(parsed[2].is_text, " event mention")
guard case .text(let t1) = parsed[0] else {
@ -461,8 +465,8 @@ class ReplyTests: XCTestCase {
}
func testParseInvalidMention() throws {
let parsed = parse_note_content(content: "this is #[0] a mention", tags: []).blocks
let parsed = parse_note_content(content: .content("this is #[0] a mention",nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 3)
XCTAssertEqual(parsed[0].is_text, "this is ")

Some files were not shown because too many files have changed in this diff Show More