Skip to content

Commit

Permalink
Add support for NIP-32 Labeling (#195)
Browse files Browse the repository at this point in the history
  • Loading branch information
tyiu authored Nov 3, 2024
1 parent 6063387 commit 88593b6
Show file tree
Hide file tree
Showing 7 changed files with 419 additions and 5 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The following [NIPs](https://github.com/nostr-protocol/nips) are implemented:
- [ ] [NIP-29: Relay-based Groups](https://github.com/nostr-protocol/nips/blob/master/29.md)
- [x] [NIP-30: Custom Emoji](https://github.com/nostr-protocol/nips/blob/master/30.md)
- [x] [NIP-31: Dealing with Unknown Events](https://github.com/nostr-protocol/nips/blob/master/31.md)
- [ ] [NIP-32: Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md)
- [x] [NIP-32: Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md)
- [ ] [NIP-34: `git` stuff](https://github.com/nostr-protocol/nips/blob/master/34.md)
- [ ] [NIP-35: Torrents](https://github.com/nostr-protocol/nips/blob/master/35.md)
- [ ] [NIP-36: Sensitive Content](https://github.com/nostr-protocol/nips/blob/master/36.md)
Expand Down
10 changes: 9 additions & 1 deletion Sources/NostrSDK/EventKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha
///
/// See [NIP-56](https://github.com/nostr-protocol/nips/blob/b4cdc1a73d415c79c35655fa02f5e55cd1f2a60c/56.md#nip-56).
case report


/// This kind of event attaches labels to label targets. This allow sof rlabeling of events, people, relays, or topics.
///
/// See [NIP-32 Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md).
case label

/// This kind of event contains a list of things the user does not want to see, such as pubkeys, hashtags, words, and event ids (threads).
///
/// See [NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md#standard-lists)
Expand Down Expand Up @@ -148,6 +153,7 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha
.genericRepost,
.giftWrap,
.report,
.label,
.muteList,
.relayListMetadata,
.bookmarksList,
Expand Down Expand Up @@ -182,6 +188,7 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha
case .genericRepost: return 16
case .giftWrap: return 1059
case .report: return 1984
case .label: return 1985
case .muteList: return 10000
case .relayListMetadata: return 10002
case .bookmarksList: return 10003
Expand Down Expand Up @@ -210,6 +217,7 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha
case .genericRepost: return GenericRepostEvent.self
case .giftWrap: return GiftWrapEvent.self
case .report: return ReportEvent.self
case .label: return LabelEvent.self
case .muteList: return MuteListEvent.self
case .relayListMetadata: return RelayListMetadataEvent.self
case .bookmarksList: return BookmarksListEvent.self
Expand Down
230 changes: 230 additions & 0 deletions Sources/NostrSDK/Events/LabelEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
//
// LabelEvent.swift
// NostrSDK
//
// Created by Terry Yiu on 10/31/24.
//

import Foundation

/// This event attaches labels to label targets. This allows for labeling of events, people, relays, or topics.
/// This supports several use cases, including distributed moderation, collection management, license assignment, and content classification.
///
/// See [NIP-32 Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md).
public final class LabelEvent: NostrEvent {

public required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}

@available(*, unavailable, message: "This initializer is unavailable for this class.")
override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) {
super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature)
}

@available(*, unavailable, message: "This initializer is unavailable for this class.")
required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) {
super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey)
}

@available(*, unavailable, message: "This initializer is unavailable for this class.")
required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws {
try super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, signedBy: keypair)
}

/// The targeted events from this label event.
public var targetedEvents: [EventTag] {
allTags(withTagName: .event).compactMap { EventTag(tag: $0) }
}

/// The targeted pubkeys from this label event.
public var targetedPubkeys: [PubkeyTag] {
allTags(withTagName: .pubkey).compactMap { PubkeyTag(tag: $0) }
}

/// The targeted event coordinates from this label event.
public var targetedEventCoordinates: [EventCoordinates] {
referencedEventCoordinates
}

/// The targeted relay URLs from this label event.
public var targetedRelayURLs: [URL] {
tags.filter { $0.name == "r" }.compactMap { URL(string: $0.value) }
}

/// The targeted topics from this label event.
public var targetedTopics: [String] {
allValues(forTagName: .hashtag)
}
}

/// Interprets label tags on an event.
///
/// If this is a ``LabelEvent`` kind (1985), the label is attached to the label target.
/// Otherwise, the label is attached to this event itself as the target.
///
/// See [NIP-32 Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md).
public protocol LabelTagInterpreting: NostrEvent {}
public extension LabelTagInterpreting {
/// The label namespaces.
var labelNamespaces: [String] {
allValues(forTagName: .labelNamespace)
}

/// Dictionary of label namespaces or marks mapped to list of labels.
/// If a label does not include mark, `ugc` (user generated content) is implied and keyed from `ugc` in the dictionary.
var labels: [String: [String]] {
let filteredTags = allTags(withTagName: .label)
return Dictionary(
grouping: filteredTags,
by: { $0.otherParameters.first ?? "ugc" }
).mapValues { tags in
tags.map { tag in tag.value }
}
}

/// The labels that include the specified mark.
/// If no mark is provided to this function or on a label tag, `ugc` (user generated content) is implied.
func labels(for mark: String?) -> [String] {
let resolvedMark = mark ?? "ugc"
return allTags(withTagName: .label)
.filter { labelTag in
let labelMark = labelTag.otherParameters.first ?? "ugc"
return labelMark == resolvedMark
}.map { $0.value }
}
}

public extension LabelEvent {
/// Builder of a ``LabelEvent``.
final class Builder: NostrEvent.Builder<LabelEvent>, RelayURLValidating {
public init() {
super.init(kind: .label)
}

/// Adds an event as a label target.
@discardableResult
public final func target(eventId: String, relayURL: URL? = nil) throws -> Builder {
appendTags(try EventTag(eventId: eventId, relayURL: relayURL).tag)
}

/// Adds a pubkey as a label target.
@discardableResult
public final func target(pubkey: String, relayURL: URL? = nil) throws -> Builder {
if let relayURL {
let validatedRelayURL = try validateRelayURL(relayURL)
appendTags(.pubkey(pubkey, otherParameters: [validatedRelayURL.absoluteString]))
} else {
appendTags(.pubkey(pubkey))
}
return self
}

/// Adds event coordinates as a label target.
@discardableResult
public final func target(eventCoordinates: EventCoordinates) throws -> Builder {
appendTags(eventCoordinates.tag)
}

/// Adds a relay URL as a label target.
@discardableResult
public final func target(relayURL: URL) throws -> Builder {
let validatedRelayURL = try validateRelayURL(relayURL)
return appendTags(Tag(name: "r", value: validatedRelayURL.absoluteString))
}

/// Adds a hashtag topic as a label target.
@discardableResult
public final func target(topic: String) throws -> Builder {
appendTags(.hashtag(topic))
}
}
}

/// Builder that labels a target.
///
/// If this is a ``LabelEvent`` kind (1985), the label is attached to the label target.
/// Otherwise, the label is attached to this event itself as the target.
///
/// See [NIP-32 Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md).
public protocol LabelBuilding: NostrEventBuilding {}
public extension LabelBuilding {
/// Labels an event in a given namespace.
///
/// Namespaces can be any string but SHOULD be unambiguous by using a well-defined namespace (such as an ISO standard) or reverse domain name notation.
///
/// Namespaces are RECOMMENDED in order to support searching by namespace rather than by a specific tag.
/// The special `ugc` ("user generated content") namespace MAY be used when the label content is provided by an end user.
///
/// Namespaces starting with # indicate that the label target should be associated with the label's value.
/// This is a way of attaching standard nostr tags to events, pubkeys, relays, urls, etc.
///
/// If this is a ``LabelEvent`` kind (1985), the label is attached to the label target.
/// Otherwise, the label is attached to this event itself as the target.
///
/// See [NIP-32 Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md).
@discardableResult
func appendLabels(_ labels: String..., namespace: String) -> Self {
self.appendLabels(contentsOf: labels, namespace: namespace)
}

/// Labels an event in a given namespace.
///
/// Namespaces can be any string but SHOULD be unambiguous by using a well-defined namespace (such as an ISO standard) or reverse domain name notation.
///
/// Namespaces are RECOMMENDED in order to support searching by namespace rather than by a specific tag.
/// The special `ugc` ("user generated content") namespace MAY be used when the label content is provided by an end user.
///
/// Namespaces starting with # indicate that the label target should be associated with the label's value.
/// This is a way of attaching standard nostr tags to events, pubkeys, relays, urls, etc.
///
/// If this is a ``LabelEvent`` kind (1985), the label is attached to the label target.
/// Otherwise, the label is attached to this event itself as the target.
///
/// See [NIP-32 Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md).
@discardableResult
func appendLabels(contentsOf labels: [String], namespace: String) -> Self {
guard !labels.isEmpty else {
return self
}

appendTags(Tag(name: .labelNamespace, value: namespace))
for label in labels {
appendTags(Tag(name: .label, value: label, otherParameters: [namespace]))
}
return self
}

/// Labels the event with a given mark.
/// A mark SHOULD be included. If it is not included, `ugc` (user generated content) is implied.
///
/// If this is a ``LabelEvent`` kind (1985), the label is attached to the label target.
/// Otherwise, the label is attached to this event itself as the target.
///
/// See [NIP-32 Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md).
@discardableResult
func appendLabels(_ labels: String..., mark: String? = nil) -> Self {
self.appendLabels(contentsOf: labels, mark: mark)
}

/// Labels the event with a given mark.
/// A mark SHOULD be included. If it is not included, `ugc` (user generated content) is implied.
///
/// If this is a ``LabelEvent`` kind (1985), the label is attached to the label target.
/// Otherwise, the label is attached to this event itself as the target.
///
/// See [NIP-32 Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md).
@discardableResult
func appendLabels(contentsOf labels: [String], mark: String? = nil) -> Self {
let otherParameters: [String]
if let mark {
otherParameters = [mark]
} else {
otherParameters = []
}
for label in labels {
appendTags(Tag(name: .label, value: label, otherParameters: otherParameters))
}
return self
}
}
4 changes: 2 additions & 2 deletions Sources/NostrSDK/Events/NostrEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
/// A structure that describes a Nostr event.
///
/// > Note: [NIP-01 Specification](https://github.com/nostr-protocol/nips/blob/master/01.md#events-and-signatures)
public class NostrEvent: Codable, Equatable, Hashable {
public class NostrEvent: Codable, Equatable, Hashable, LabelTagInterpreting {
public static func == (lhs: NostrEvent, rhs: NostrEvent) -> Bool {
lhs.id == rhs.id &&
lhs.pubkey == rhs.pubkey &&
Expand Down Expand Up @@ -318,7 +318,7 @@ public protocol NostrEventBuilding {

public extension NostrEvent {
/// Builder of a ``NostrEvent`` of type `T`.
class Builder<T: NostrEvent>: NostrEventBuilding {
class Builder<T: NostrEvent>: NostrEventBuilding, LabelBuilding {
public typealias EventType = T

/// The event kind.
Expand Down
85 changes: 85 additions & 0 deletions Sources/NostrSDK/Events/Tags/PubkeyTag.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//
// PubkeyTag.swift
// NostrSDK
//
// Created by Terry Yiu on 10/31/24.
//

import Foundation

public struct PubkeyTag: RelayProviding, RelayURLValidating, Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.tag == rhs.tag
}

/// The ``Tag`` that represents this pubkey tag.
public let tag: Tag

/// The pubkey being referenced.
public var pubkey: String {
tag.value
}

/// The URL of a recommended relay associated with the reference.
public var relayURL: URL? {
guard let relayString = tag.otherParameters.first, !relayString.isEmpty else {
return nil
}

return try? validateRelayURLString(relayString)
}

/// The petname of the pubkey.
public var petname: String? {
guard tag.otherParameters.count >= 2 else {
return nil
}

return tag.otherParameters[1]
}

/// Initializes an event tag from a ``Tag``.
/// `nil` is returned if the tag is not an pubkey tag.
public init?(tag: Tag) {
guard tag.name == TagName.pubkey.rawValue else {
return nil
}

self.tag = tag
}

/// Initializes a pubkey tag.
/// - Parameters:
/// - publicKey: The ``PublicKey`` being referenced.
/// - relayURL: The URL of a recommended relay associated with the reference.
/// - petname: The petname of the pubkey.
public init(publicKey: PublicKey, relayURL: URL? = nil, petname: String? = nil) throws {
let validatedRelayURL: URL?
if let relayURL {
validatedRelayURL = try RelayURLValidator.shared.validateRelayURL(relayURL)
} else {
validatedRelayURL = nil
}

var tagOtherParameters = [validatedRelayURL?.absoluteString ?? ""]

if let petname {
tagOtherParameters.append(petname)
}

tag = .pubkey(publicKey.hex, otherParameters: tagOtherParameters)
}

/// Initializes a pubkey tag.
/// - Parameters:
/// - pubkey: The hex pubkey being referenced.
/// - relayURL: The URL of a recommended relay associated with the reference.
/// - petname: The petname of the pubkey.
public init(pubkey: String, relayURL: URL? = nil, petname: String? = nil) throws {
guard let publicKey = PublicKey(hex: pubkey) else {
throw EventCreatingError.invalidInput
}

try self.init(publicKey: publicKey, relayURL: relayURL, petname: petname)
}
}
Loading

0 comments on commit 88593b6

Please sign in to comment.