1
0
mirror of git://jb55.com/damus synced 2024-09-28 16:00:43 +00:00

nip10: marker replies

This should drastically increase compatibility for damus replies in
other clients.

Also filter non-pubkey references when replying so we don't run into the
q-tag bug.

Changelog-Added: Added nip10 marker replies
Changelog-Fixed: Fixed issue where some replies were including the q tag
Fixes: https://github.com/damus-io/damus/issues/2239
Fixes: https://github.com/damus-io/damus/issues/2233
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin 2024-05-09 13:33:04 -07:00
parent 0b199a18b4
commit 514a053dce
10 changed files with 231 additions and 28 deletions

View File

@ -175,6 +175,7 @@
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */; };
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; };
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; };
4C45E5022BED4D000025A428 /* ThreadReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C45E5012BED4D000025A428 /* ThreadReply.swift */; };
4C463CBF2B960B96008A8C36 /* PurpleBackdrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C463CBE2B960B96008A8C36 /* PurpleBackdrop.swift */; };
4C4793012A993CDA00489948 /* mdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4793002A993B9A00489948 /* mdb.c */; settings = {COMPILER_FLAGS = "-w"; }; };
4C4793042A993DC000489948 /* midl.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4793032A993DB900489948 /* midl.c */; settings = {COMPILER_FLAGS = "-w"; }; };
@ -248,6 +249,7 @@
4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */; };
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */; };
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; };
4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C45E5012BED4D000025A428 /* ThreadReply.swift */; };
4C9054852A6AEAA000811EEC /* NdbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9054842A6AEAA000811EEC /* NdbTests.swift */; };
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
@ -994,6 +996,7 @@
4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesView.swift; sourceTree = "<group>"; };
4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceView.swift; sourceTree = "<group>"; };
4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; };
4C45E5012BED4D000025A428 /* ThreadReply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReply.swift; sourceTree = "<group>"; };
4C463CBE2B960B96008A8C36 /* PurpleBackdrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleBackdrop.swift; sourceTree = "<group>"; };
4C478E242A9932C100489948 /* Ndb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ndb.swift; sourceTree = "<group>"; };
4C478E262A99353500489948 /* threadpool.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = threadpool.h; sourceTree = "<group>"; };
@ -1615,7 +1618,6 @@
4C363A93282704FA006E126D /* Post.swift */,
4C363A952827096D006E126D /* PostBlock.swift */,
4C363A9928283854006E126D /* Reply.swift */,
4C363A9B282838B9006E126D /* EventRef.swift */,
4C363AA328296DEE006E126D /* SearchModel.swift */,
0E8A4BB62AE4359200065E81 /* NostrFilter+Hashable.swift */,
4C3AC79A28306D7B00E1F516 /* Contacts.swift */,
@ -1788,6 +1790,15 @@
path = flatbuffers;
sourceTree = "<group>";
};
4C45E5002BED4CE10025A428 /* NIP10 */ = {
isa = PBXGroup;
children = (
4C363A9B282838B9006E126D /* EventRef.swift */,
4C45E5012BED4D000025A428 /* ThreadReply.swift */,
);
path = NIP10;
sourceTree = "<group>";
};
4C478E2A2A9935D300489948 /* bindings */ = {
isa = PBXGroup;
children = (
@ -2486,6 +2497,7 @@
4CE6DEE527F7A08100C66700 /* damus */ = {
isa = PBXGroup;
children = (
4C45E5002BED4CE10025A428 /* NIP10 */,
4C1D4FB32A7967990024F453 /* build-git-hash.txt */,
4CA3529C2A76AE47003BB08B /* Notify */,
4CC14FEC2A73FC9A007AEB17 /* Types */,
@ -3222,6 +3234,7 @@
4C86F7C62A76C51100EC0817 /* AttachedWalletNotify.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */,
4C45E5022BED4D000025A428 /* ThreadReply.swift in Sources */,
D74AAFD42B155ECB006CF0F4 /* Zaps+.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
4C7D09592A05BEAD00943473 /* KeyboardVisible.swift in Sources */,
@ -3589,6 +3602,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */,
D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */,
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */,
D7CB5D552B11758A00AD4105 /* UnmuteThreadNotify.swift in Sources */,

View File

@ -292,9 +292,8 @@ func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
}
func post_to_event(post: NostrPost, keypair: FullKeypair) -> NostrEvent? {
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 post_tags = make_post_tags(post_blocks: post_blocks, tags: post.tags)
let content = post_tags.blocks
.map(\.asString)
.joined(separator: "")

View File

@ -10,12 +10,10 @@ import Foundation
struct NostrPost {
let kind: NostrKind
let content: String
let references: [RefId]
let tags: [[String]]
init(content: String, references: [RefId], kind: NostrKind = .text, tags: [[String]] = []) {
init(content: String, kind: NostrKind = .text, tags: [[String]] = []) {
self.content = content
self.references = references
self.kind = kind
self.tags = tags
}

View File

@ -0,0 +1,58 @@
//
// ThreadReply.swift
// damus
//
// Created by William Casarin on 2024-05-09.
//
import Foundation
struct ThreadReply {
let root: NoteRef
let reply: NoteRef?
let mention: Mention<NoteRef>?
var is_reply_to_root: Bool {
guard let reply else {
// if we have no reply and only root then this is reply-to-root,
// but it should never really be in this form...
return true
}
return root.id == reply.id
}
init(root: NoteRef, reply: NoteRef?, mention: Mention<NoteRef>?) {
self.root = root
self.reply = reply
self.mention = mention
}
init?(event_refs: [EventRef]) {
var root: NoteRef? = nil
var reply: NoteRef? = nil
var mention: Mention<NoteRef>? = nil
for evref in event_refs {
switch evref {
case .mention(let m):
mention = m
case .thread_id(let r):
root = r
case .reply(let r):
reply = r
case .reply_to_root(let r):
root = r
reply = r
}
}
// nip10 threads must have a root
guard let root else {
return nil
}
self = ThreadReply(root: root, reply: reply, mention: mention)
}
}

View File

@ -92,13 +92,24 @@ struct PostView: View {
}
func send_post() {
let refs = references.filter { ref in
if case .pubkey(let pk) = ref, filtered_pubkeys.contains(pk) {
return false
// don't add duplicate pubkeys but retain order
var pkset = Set<Pubkey>()
// we only want pubkeys really
let pks = references.reduce(into: Array<Pubkey>()) { acc, ref in
guard case .pubkey(let pk) = ref else {
return
}
return true
if pkset.contains(pk) || filtered_pubkeys.contains(pk) {
return
}
pkset.insert(pk)
acc.append(pk)
}
let new_post = build_post(state: damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, references: refs)
let new_post = build_post(state: damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, pubkeys: pks)
notify(.post(.post(new_post)))
@ -604,7 +615,29 @@ private func isAlphanumeric(_ char: Character) -> Bool {
return char.isLetter || char.isNumber
}
func build_post(state: DamusState, post: NSMutableAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId]) -> NostrPost {
func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair) -> [[String]] {
guard let nip10 = replying_to.thread_reply(keypair) else {
// we're replying to a post that isn't in a thread,
// just add a single reply-to-root tag
return [["e", replying_to.id.hex(), "", "root"]]
}
// otherwise use the root tag from the parent's nip10 reply and include the note
// that we are replying to's note id.
var tags = [
["e", nip10.root.note_id.hex(), nip10.root.relay ?? "", "root"],
["e", replying_to.id.hex(), "", "reply"]
]
// we also add the parent's nip10 reply tag as an additional e tag for context
if let reply = nip10.reply {
tags.append(["e", reply.note_id.hex(), reply.relay ?? ""])
}
return tags
}
func build_post(state: DamusState, post: NSMutableAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], pubkeys: [Pubkey]) -> NostrPost {
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
if let link = attributes[.link] as? String {
let nextCharIndex = range.upperBound
@ -634,20 +667,35 @@ func build_post(state: DamusState, post: NSMutableAttributedString, action: Post
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
var tags = uploadedMedias.compactMap { $0.metadata?.to_tag() }
if !imagesString.isEmpty {
content.append(" " + imagesString + " ")
}
if case .quoting(let ev) = action {
var tags: [[String]] = []
switch action {
case .replying_to(let replying_to):
// start off with the reply tags
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
case .quoting(let ev):
content.append(" nostr:" + bech32_note_id(ev.id))
if let quoted_ev = state.events.lookup(ev.id) {
tags.append(["p", quoted_ev.pubkey.hex()])
}
case .posting(let postTarget):
break
}
// include pubkeys
tags += pubkeys.map { pk in
["p", pk.hex()]
}
return NostrPost(content: content, references: references, kind: .text, tags: tags)
// append additional tags
tags += uploadedMedias.compactMap { $0.metadata?.to_tag() }
return NostrPost(content: content, kind: .text, tags: tags)
}

View File

@ -127,9 +127,92 @@ final class NIP10Tests: XCTestCase {
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [root_note_id])
let nip10 = note.thread_reply(test_keypair)!
XCTAssertEqual(nip10.is_reply_to_root, true)
XCTAssertEqual(nip10.root.note_id, root_note_id)
XCTAssertEqual(nip10.reply!.note_id, root_note_id)
}
// seen in the wild by the gleasonator
func test_single_marker() {
let root_note_id_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52"
let tags = [
["e", root_note_id_hex, "", "reply"],
]
let root_note_id = NoteId(hex: root_note_id_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
let thread_reply = ThreadReply(event_refs: refs)!
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(thread_reply.mention, nil)
XCTAssertEqual(thread_reply.root.note_id, root_note_id)
XCTAssertEqual(thread_reply.reply!.note_id, root_note_id)
XCTAssertEqual(thread_reply.is_reply_to_root, true)
}
func test_marker_reply() {
let note_json = """
{
"pubkey": "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e",
"content": "Cant zap you btw",
"id": "a8dc8b74852d7ad114d5d650b2125459c0cba3c1fdcaaf527e03f24082e11ab3",
"created_at": 1715275773,
"sig": "4ee5d8f954c6c087ce51ad02d30dd226eea939cd9ef4e8a8ce4bfaf3aba0a852316cfda83ce3fc9a3d98392a738e7c6b036a3b2aced1392db1be3ca190835a17",
"kind": 1,
"tags": [
[
"e",
"1bb940ce0ba0d4a3b2a589355d908498dcd7452f941cf520072218f7e6ede75e",
"wss://relay.nostrplebs.com",
"reply"
],
[
"p",
"6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e"
],
[
"e",
"00152d2945459fb394fed2ea95af879c903c4ec42d96327a739fa27c023f20e0",
"wss://nostr.mutinywallet.com/",
"root"
]
]
}
""";
let replying_to_hex = "a8dc8b74852d7ad114d5d650b2125459c0cba3c1fdcaaf527e03f24082e11ab3"
let pk = Pubkey(hex: "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e")!
let last_reply_hex = "1bb940ce0ba0d4a3b2a589355d908498dcd7452f941cf520072218f7e6ede75e"
let note = decode_nostr_event_json(json: note_json)!
let reply = build_post(state: test_damus_state, post: .init(string: "hello"), action: .replying_to(note), uploadedMedias: [], pubkeys: [pk] + note.referenced_pubkeys.map({pk in pk}))
let root_hex = "00152d2945459fb394fed2ea95af879c903c4ec42d96327a739fa27c023f20e0"
XCTAssertEqual(reply.tags,
[
["e", root_hex, "wss://nostr.mutinywallet.com/", "root"],
["e", replying_to_hex, "", "reply"],
["e", last_reply_hex, "wss://relay.nostrplebs.com"],
["p", "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e"],
["p", "6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e"],
])
}
func test_mixed_nip10() {
let root_note_id_hex = "27e71cf53299dafb5dc7bcc0a078357418a4375cb1097bf5184662493f79a627"
let reply_hex = "1a616998552cf76e9786f76ac68f6104cdae46377330735c68bfe0b9426d2fa8"

View File

@ -123,7 +123,7 @@ class ReplyTests: XCTestCase {
post.append(user_tag_attr_string(profile: profile, pubkey: pk))
post.append(.init(string: "\n"))
let post_note = build_post(state: test_damus_state, post: post, action: .posting(.none), uploadedMedias: [], references: [.pubkey(pk)])
let post_note = build_post(state: test_damus_state, post: post, action: .posting(.none), uploadedMedias: [], pubkeys: [pk])
let expected_render = "nostr:\(pk.npub)\nnostr:\(pk.npub)"
XCTAssertEqual(post_note.content, expected_render)
@ -315,7 +315,7 @@ class ReplyTests: XCTestCase {
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: [.event(evid)])
let post = NostrPost(content: content, tags: [["e", evid.hex()]])
let ev = post_to_event(post: post, keypair: test_keypair_full)!
XCTAssertEqual(ev.tags.count, 2)
@ -330,7 +330,7 @@ class ReplyTests: XCTestCase {
let nsec = "nsec1jmzdz7d0ldqctdxwm5fzue277ttng2pk28n2u8wntc2r4a0w96ssnyukg7"
let content = "this is a @\(nsec) mention"
let blocks = parse_post_blocks(content: content)
let post = NostrPost(content: content, references: [.event(evid)])
let post = NostrPost(content: content, tags: [["e", evid.hex()]])
let ev = post_to_event(post: post, keypair: test_keypair_full)!
XCTAssertEqual(ev.tags.count, 2)
@ -344,13 +344,13 @@ class ReplyTests: XCTestCase {
let thread_id = NoteId(hex: "a250fc93570c3e87f9c9b08d6b3ef7b8e05d346df8a52c69e30ffecdb178fb9e")!
let reply_id = NoteId(hex: "9a180a10f16dac9566543ad1fc29616aab272b0cf123ab5d58843e16f4ef03a3")!
let refs: [RefId] = [
.event(thread_id),
.event(reply_id),
.pubkey(pubkey)
let tags = [
["e", thread_id.hex()],
["e", reply_id.hex()],
["p", pubkey.hex()]
]
let post = NostrPost(content: "this is a (@\(pubkey.npub)) mention", references: refs)
let post = NostrPost(content: "this is a (@\(pubkey.npub)) mention", tags: tags)
let ev = post_to_event(post: post, keypair: test_keypair_full)!
XCTAssertEqual(ev.content, "this is a (nostr:\(pubkey.npub)) mention")

View File

@ -192,7 +192,7 @@ class damusTests: XCTestCase {
*/
func testMakeHashtagPost() {
let post = NostrPost(content: "#damus some content #bitcoin derp #かっこいい wow", references: [])
let post = NostrPost(content: "#damus some content #bitcoin derp #かっこいい wow", tags: [])
let ev = post_to_event(post: post, keypair: test_keypair_full)!
XCTAssertEqual(ev.tags.count, 3)
@ -269,7 +269,7 @@ class damusTests: XCTestCase {
}
private func createEventFromContentString(_ content: String) -> NostrEvent {
let post = NostrPost(content: content, references: [])
let post = NostrPost(content: content, tags: [])
guard let ev = post_to_event(post: post, keypair: test_keypair_full) else {
XCTFail("Could not create event")
return test_note

View File

@ -341,8 +341,11 @@ extension NdbNote {
}
func event_refs(_ keypair: Keypair) -> [EventRef] {
let refs = interpret_event_refs_ndb(blocks: self.blocks(keypair).blocks, tags: self.tags)
return refs
return interpret_event_refs_ndb(blocks: self.blocks(keypair).blocks, tags: self.tags)
}
func thread_reply(_ keypair: Keypair) -> ThreadReply? {
ThreadReply(event_refs: event_refs(keypair))
}
func get_content(_ keypair: Keypair) -> String {