// // ReferencedId.swift // damus // // Created by William Casarin on 2023-07-22. // import Foundation func tagref_should_be_id(_ tag: NdbTagElem) -> Bool { return !(tag.matches_char("t") || tag.matches_char("d")) } struct References: Sequence, IteratorProtocol { let tags: TagsSequence var tags_iter: TagsIterator init(tags: TagsSequence) { self.tags = tags self.tags_iter = tags.makeIterator() } mutating func next() -> T? { while let tag = tags_iter.next() { guard let evref = T.from_tag(tag: tag) else { continue } return evref } return nil } } 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 { return self } func first_char() -> AsciiCharacter? { self.first.flatMap { chr in AsciiCharacter(chr) } } func matches_char(_ c: AsciiCharacter) -> Bool { return self.first == c.character } func matches_str(_ str: String) -> Bool { return self == str } } enum FollowRef: TagKeys, Hashable, TagConvertible, Equatable { // 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 } } enum FollowKeys: AsciiCharacter, TagKey, CustomStringConvertible { case p, t var keychar: AsciiCharacter { self.rawValue } var description: String { self.rawValue.description } } 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 } } } /// Models common tag references defined by the Nostr protocol, and their associated values. /// /// For example, this raw JSON tag sequence: /// ```json /// ["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"] /// ``` /// /// would be parsed into something equivalent to `.pubkey(Pubkey(hex: "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"))` /// /// ## Notes /// /// - Not all tag information from all NIPs can be modelled using this alone, as some NIPs may define extra associated values for specific event types. You may need to use a specialized type for some event kinds /// enum RefId: TagConvertible, TagKeys, Equatable, Hashable { case event(NoteId) case pubkey(Pubkey) case quote(QuoteId) case hashtag(Hashtag) case param(TagElem) case naddr(NAddr) case reference(String) /// The key that defines the type of reference being made 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 case .naddr: return .a case .reference: return .r } } /// Defines the type of reference being made on a Nostr event tag /// /// Example: /// ```json /// ["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"] /// ``` /// /// The `RefKey` is "p" enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible { case e, p, t, d, q, a, r var keychar: AsciiCharacter { self.rawValue } var description: String { self.keychar.description } } /// A raw nostr-style tag sequence representation of this object var tag: [String] { [self.key.description, self.description] } /// Describes what is being referenced, as a `String` 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.hashtag case .param(let string): return string.string() case .naddr(let naddr): return naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier case .reference(let string): return string } } /// Parses a raw tag sequence 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(Hashtag(hashtag: t1.string())) case .d: return .param(t1) case .a: return .naddr(NAddr(identifier: "", author: Pubkey(Data()), relays: [], kind: 0)) case .r: return .reference(t1.string()) } } }