1
0
mirror of git://jb55.com/damus synced 2024-10-01 09:20:47 +00:00

nip19: add bech32 TLV url parsing

Create shortened URLs for bech32 with TLV data strings. Additionally,
upon clicking on an nevent URL the user is directed to the note.

Lightning-url: LNURL1DP68GURN8GHJ7EM9W3SKCCNE9E3K7MF0D3H82UNVWQHKWUN9V4HXGCTHDC6RZVGR8SW3G
Signed-off-by: kernelkind <kernelkind@gmail.com>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
kernelkind 2024-01-18 14:59:27 -05:00 committed by William Casarin
parent af75eed83a
commit d07ad67778
9 changed files with 285 additions and 88 deletions

View File

@ -492,21 +492,15 @@ struct ContentView: View {
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
}
openEvent(noteId: noteId, notificationType: local.type)
case .nevent(let nevent):
openEvent(noteId: nevent.noteid, notificationType: local.type)
case .nprofile(let nprofile):
open_profile(pubkey: nprofile.author)
case .nrelay(_):
break
case .naddr(let naddr):
break
}
@ -725,6 +719,22 @@ struct ContentView: View {
}
}
private func openEvent(noteId: NoteId, notificationType: LocalNotificationType) {
guard let target = damus_state.events.lookup(noteId) else {
return
}
switch notificationType {
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:
break
}
}
}
struct ContentView_Previews: PreviewProvider {
@ -1082,6 +1092,8 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
case .param, .quote:
// doesn't really make sense here
break
case .naddr(let naddr):
break // TODO: fix
}
case .filter(let filt):
result(.filter(filt))

View File

@ -113,7 +113,7 @@ func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
case let (.pubkey(pk), .pubkey(follow_pk)):
return pk == follow_pk
case (.hashtag, .pubkey), (.pubkey, .hashtag),
(.event, _), (.quote, _), (.param, _):
(.event, _), (.quote, _), (.param, _), (.naddr, _):
return false
}
}

View File

@ -10,6 +10,8 @@ import Foundation
enum MentionType: AsciiCharacter, TagKey {
case p
case e
case a
case r
var keychar: AsciiCharacter {
self.rawValue
@ -17,21 +19,26 @@ enum MentionType: AsciiCharacter, TagKey {
}
enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
case pubkey(Pubkey) // TODO: handle nprofile
case pubkey(Pubkey)
case note(NoteId)
case nevent(NEvent)
case nprofile(NProfile)
case nrelay(String)
case naddr(NAddr)
var key: MentionType {
switch self {
case .pubkey: return .p
case .note: return .e
case .nevent: return .e
case .nprofile: return .p
case .nrelay: return .r
case .naddr: return .a
}
}
var bech32: String {
switch self {
case .pubkey(let pubkey): return bech32_pubkey(pubkey)
case .note(let noteId): return bech32_note_id(noteId)
}
return Bech32Object.encode(toBech32Object())
}
static func from_bech32(str: String) -> MentionRef? {
@ -46,6 +53,10 @@ enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
switch self {
case .pubkey(let pubkey): return pubkey
case .note: return nil
case .nevent(let nevent): return nevent.author
case .nprofile(let nprofile): return nprofile.author
case .nrelay: return nil
case .naddr: return nil
}
}
@ -53,6 +64,10 @@ enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
switch self {
case .pubkey(let pubkey): return ["p", pubkey.hex()]
case .note(let noteId): return ["e", noteId.hex()]
case .nevent(let nevent): return ["e", nevent.noteid.hex()]
case .nprofile(let nprofile): return ["p", nprofile.author.hex()]
case .nrelay(let url): return ["r", url]
case .naddr(let naddr): return ["a", naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier.string()]
}
}
@ -64,14 +79,45 @@ enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
guard let t0 = i.next(),
let chr = t0.single_char,
let mention_type = MentionType(rawValue: chr),
let id = i.next()?.id()
let element = i.next()
else {
return nil
}
switch mention_type {
case .p: return .pubkey(Pubkey(id))
case .e: return .note(NoteId(id))
case .p:
guard let data = element.id() else { return nil }
return .pubkey(Pubkey(data))
case .e:
guard let data = element.id() else { return nil }
return .note(NoteId(data))
case .a:
let str = element.string()
let data = str.split(separator: ":")
if(data.count != 3) { return nil }
guard let pubkey = Pubkey(hex: String(data[1])) else { return nil }
guard let kind = UInt32(data[0]) else { return nil }
return .naddr(NAddr(identifier: String(data[2]), author: pubkey, relays: [], kind: kind))
case .r: return .nrelay(element.string())
}
}
func toBech32Object() -> Bech32Object {
switch self {
case .pubkey(let pk):
return .npub(pk)
case .note(let noteid):
return .note(noteid)
case .naddr(let naddr):
return .naddr(naddr)
case .nevent(let nevent):
return .nevent(nevent)
case .nprofile(let nprofile):
return .nprofile(nprofile)
case .nrelay(let url):
return .nrelay(url)
}
}
}
@ -251,4 +297,3 @@ func post_to_event(post: NostrPost, keypair: FullKeypair) -> NostrEvent? {
.joined(separator: "")
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: post.kind.rawValue, tags: post_tags.tags)
}

View File

@ -182,26 +182,31 @@ func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage)
astr.append(wrapped)
}
func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText {
switch m.ref {
case .pubkey(let pk):
let npub = bech32_pubkey(pk)
let profile_txn = profiles.lookup(id: pk)
let profile = profile_txn?.unsafeUnownedValue
let disp = Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50)
var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "damus:nostr:\(npub)")
attributedString.foregroundColor = DamusColors.purple
return CompatibleText(attributed: attributedString)
case .note(let note_id):
let bevid = bech32_note_id(note_id)
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
attributedString.link = URL(string: "damus:nostr:\(bevid)")
attributedString.foregroundColor = DamusColors.purple
func getDisplayName(pk: Pubkey, profiles: Profiles) -> String {
let profile_txn = profiles.lookup(id: pk)
let profile = profile_txn?.unsafeUnownedValue
return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50)
}
return CompatibleText(attributed: attributedString)
}
func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText {
let bech32String = Bech32Object.encode(m.ref.toBech32Object())
let attributedStringLiteral: String = {
switch m.ref {
case .pubkey(let pk): return getDisplayName(pk: pk, profiles: profiles)
case .note: return "@\(abbrev_pubkey(bech32String))"
case .nevent: return "@\(abbrev_pubkey(bech32String))"
case .nprofile(let nprofile): return getDisplayName(pk: nprofile.author, profiles: profiles)
case .nrelay(let url): return url
case .naddr: return "@\(abbrev_pubkey(bech32String))"
}
}()
var attributedString = AttributedString(stringLiteral: attributedStringLiteral)
attributedString.link = URL(string: "damus:nostr:\(bech32String)")
attributedString.foregroundColor = DamusColors.purple
return CompatibleText(attributed: attributedString)
}
// trim suffix whitespace and newlines

View File

@ -121,7 +121,8 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case quote(QuoteId)
case hashtag(TagElem)
case param(TagElem)
case naddr(NAddr)
var key: RefKey {
switch self {
case .event: return .e
@ -129,11 +130,12 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case .quote: return .q
case .hashtag: return .t
case .param: return .d
case .naddr: return .a
}
}
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
case e, p, t, d, q
case e, p, t, d, q, a
var keychar: AsciiCharacter {
self.rawValue
@ -155,6 +157,8 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case .quote(let quote): return quote.hex()
case .hashtag(let string): return string.string()
case .param(let string): return string.string()
case .naddr(let naddr):
return naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier
}
}
@ -174,6 +178,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case .q: return t1.id().map({ .quote(QuoteId($0)) })
case .t: return .hashtag(t1)
case .d: return .param(t1)
case .a: return .naddr(NAddr(identifier: "", author: Pubkey(Data()), relays: [], kind: 0))
}
}
}

View File

@ -150,47 +150,18 @@ fileprivate extension Block {
self = .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
}
}
fileprivate extension Block {
/// Failable initializer for the C-backed type `mention_bech32_block_t`. This initializer will inspect the
/// bech32 type code and build the appropriate enum type.
init?(bech32 b: mention_bech32_block_t) {
switch b.bech32.type {
case NOSTR_BECH32_NOTE:
let note = b.bech32.data.note;
let note_id = NoteId(Data(bytes: note.event_id, count: 32))
self = .mention(.any(.note(note_id)))
case NOSTR_BECH32_NEVENT:
let nevent = b.bech32.data.nevent;
let note_id = NoteId(Data(bytes: nevent.event_id, count: 32))
self = .mention(.any(.note(note_id)))
case NOSTR_BECH32_NPUB:
let npub = b.bech32.data.npub
let pubkey = Pubkey(Data(bytes: npub.pubkey, count: 32))
self = .mention(.any(.pubkey(pubkey)))
case NOSTR_BECH32_NSEC:
let nsec = b.bech32.data.nsec
let privkey = Privkey(Data(bytes: nsec.nsec, count: 32))
guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil }
self = .mention(.any(.pubkey(pubkey)))
case NOSTR_BECH32_NPROFILE:
let nprofile = b.bech32.data.nprofile
let pubkey = Pubkey(Data(bytes: nprofile.pubkey, count: 32))
self = .mention(.any(.pubkey(pubkey)))
case NOSTR_BECH32_NRELAY:
let nrelay = b.bech32.data.nrelay
guard let relay_str = String(nrelay.relay) else {
return nil
}
self = .relay(relay_str)
case NOSTR_BECH32_NADDR:
// TODO: wtf do I do with this
guard let naddr = String(b.str) else {
return nil
}
self = .text("nostr:" + naddr)
default:
guard let decoded = decodeCBech32(b.bech32) else {
return nil
}
guard let ref = decoded.toMentionRef() else {
return nil
}
self = .mention(.any(ref))
}
}
extension Block {
@ -201,10 +172,7 @@ extension Block {
return "#[\(idx)]"
}
switch m.ref {
case .pubkey(let pk): return "nostr:\(pk.npub)"
case .note(let note_id): return "nostr:\(note_id.bech32)"
}
return "nostr:" + Bech32Object.encode(m.ref.toBech32Object())
case .relay(let relay):
return relay
case .text(let txt):

View File

@ -16,7 +16,7 @@ fileprivate extension String {
}
}
struct NEvent : Equatable {
struct NEvent : Equatable, Hashable {
let noteid: NoteId
let relays: [String]
let author: Pubkey?
@ -49,12 +49,12 @@ struct NEvent : Equatable {
}
}
struct NProfile : Equatable {
struct NProfile : Equatable, Hashable {
let author: Pubkey
let relays: [String]
}
struct NAddr : Equatable {
struct NAddr : Equatable, Hashable {
let identifier: String
let author: Pubkey
let relays: [String]
@ -107,6 +107,29 @@ enum Bech32Object : Equatable {
return bech32_encode(hrp: "nscript", data)
}
}
func toMentionRef() -> MentionRef? {
switch self {
case .nsec(let privkey):
guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil }
return .pubkey(pubkey)
case .npub(let pubkey):
return .pubkey(pubkey)
case .note(let noteid):
return .note(noteid)
case .nscript(_):
return nil
case .nevent(let nevent):
return .nevent(nevent)
case .nprofile(let nprofile):
return .nprofile(nprofile)
case .nrelay(let relayURL):
return .nrelay(relayURL)
case .naddr(let naddr):
return .naddr(naddr)
}
}
}
func decodeCBech32(_ b: nostr_bech32_t) -> Bech32Object? {

View File

@ -6,6 +6,7 @@
//
import XCTest
import SwiftUI
@testable import damus
class NoteContentViewTests: XCTestCase {
@ -35,5 +36,101 @@ class NoteContentViewTests: XCTestCase {
XCTAssertTrue((parsed.blocks[0].asURL != nil), "NoteContentView does not correctly parse an image block when url in JSON content contains optional escaped slashes.")
}
func testMentionStr_Pubkey_ContainsAbbreviated() throws {
let compatibleText = createCompatibleText(test_pubkey.npub)
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "17ldvg64:nq5mhr77")
}
func testMentionStr_Pubkey_ContainsFullBech32() {
let compatableText = createCompatibleText(test_pubkey.npub)
assertCompatibleTextHasExpectedString(compatibleText: compatableText, expected: test_pubkey.npub)
}
func testMentionStr_Nprofile_ContainsAbbreviated() throws {
let compatibleText = createCompatibleText("nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p")
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "180cvv07:wsyjh6w6")
}
func testMentionStr_Nprofile_ContainsFullBech32() throws {
let bech = "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p"
let compatibleText = createCompatibleText(bech)
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: bech)
}
func testMentionStr_Note_ContainsAbbreviated() {
let compatibleText = createCompatibleText(test_note.id.bech32)
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "note1qqq:qqn2l0z3")
}
func testMentionStr_Note_ContainsFullBech32() {
let compatableText = createCompatibleText(test_note.id.bech32)
assertCompatibleTextHasExpectedString(compatibleText: compatableText, expected: test_note.id.bech32)
}
func testMentionStr_Nevent_ContainsAbbreviated() {
let bech = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm"
let compatibleText = createCompatibleText(bech)
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "nevent1q:t5nxnepm")
}
func testMentionStr_Nevent_ContainsFullBech32() throws {
let bech = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm"
let compatibleText = createCompatibleText(bech)
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: bech)
}
func testMentionStr_Nrelay_ContainsAbbreviated() {
let bech = "nrelay1qqt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueq4r295t"
let compatibleText = createCompatibleText(bech)
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "wss://relay.nostr.band")
}
func testMentionStr_Nrelay_ContainsFullBech32() {
let bech = "nrelay1qqt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueq4r295t"
let compatibleText = createCompatibleText(bech)
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: bech)
}
func testMentionStr_Naddr_ContainsAbbreviated() {
let bech = "naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld"
let compatibleText = createCompatibleText(bech)
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "naddr1qq:3cnmhuld")
}
func testMentionStr_Naddr_ContainsFullBech32() {
let bech = "naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld"
let compatibleText = createCompatibleText(bech)
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: bech)
}
}
private func assertCompatibleTextHasExpectedString(compatibleText: CompatibleText, expected: String) {
guard let hasExpected = compatibleText.items.first?.attributed_string()?.description.contains(expected) else {
XCTFail()
return
}
XCTAssertTrue(hasExpected)
}
private func createCompatibleText(_ bechString: String) -> CompatibleText {
guard let mentionRef = Bech32Object.parse(bechString)?.toMentionRef() else {
XCTFail("Failed to create MentionRef from Bech32 string")
return CompatibleText()
}
return mention_str(.any(mentionRef), profiles: test_damus_state.profiles)
}

View File

@ -228,5 +228,47 @@ class damusTests: XCTestCase {
XCTAssertEqual(txt, "there is no mention here")
}
func testTagGeneration_Nevent_ContainsETag() {
let ev = createEventFromContentString("nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm")
XCTAssertEqual(ev.tags.count, 1)
XCTAssertEqual(ev.tags[0][0].string(), "e")
XCTAssertEqual(ev.tags[0][1].string(), "b9f5441e45ca39179320e0031cfb18e34078673dcc3d3e3a3b3a981760aa5696")
}
func testTagGeneration_Nprofile_ContainsPTag() {
let ev = createEventFromContentString("nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p")
XCTAssertEqual(ev.tags.count, 1)
XCTAssertEqual(ev.tags[0][0].string(), "p")
XCTAssertEqual(ev.tags[0][1].string(), "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d")
}
func testTagGeneration_Nrelay_ContainsRTag() {
let ev = createEventFromContentString("nrelay1qqt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueq4r295t")
XCTAssertEqual(ev.tags.count, 1)
XCTAssertEqual(ev.tags[0][0].string(), "r")
XCTAssertEqual(ev.tags[0][1].string(), "wss://relay.nostr.band")
}
func testTagGeneration_Naddr_ContainsATag(){
let ev = createEventFromContentString("naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld")
XCTAssertEqual(ev.tags.count, 1)
XCTAssertEqual(ev.tags[0][0].string(), "a")
XCTAssertEqual(ev.tags[0][1].string(), "30023:599f67f7df7694c603a6d0636e15ebc610db77dcfd47d6e5d05386d821fb3ea9:1700730909108")
}
}
private func createEventFromContentString(_ content: String) -> NostrEvent {
let post = NostrPost(content: content, references: [])
guard let ev = post_to_event(post: post, keypair: test_keypair_full) else {
XCTFail("Could not create event")
return test_note
}
return ev
}