Skip to content

Commit

Permalink
Amend NIP-52 to support calendars and RSVPs
Browse files Browse the repository at this point in the history
  • Loading branch information
tyiu committed Dec 19, 2023
1 parent 87cf347 commit 08165aa
Show file tree
Hide file tree
Showing 15 changed files with 404 additions and 58 deletions.
24 changes: 12 additions & 12 deletions Sources/NostrSDK/EventCreating.swift
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,11 @@ public extension EventCreating {
/// Creates a ``DateBasedCalendarEvent`` (kind 31922) which starts on a date and ends before a different date in the future.
/// Its use is appropriate for all-day or multi-day events where time and time zone hold no significance. e.g., anniversary, public holidays, vacation days.
/// - Parameters:
/// - name: The name of the calendar event.
/// - title: The title of the calendar event.
/// - description: A detailed description of the calendar event.
/// - startDate: An inclusive start date. Must be less than end, if it exists. If there are any components other than year, month,
/// - endDate: An exclusive end date. If omitted, the calendar event ends on the same date as start.
/// - location: The location of the calendar event. e.g. address, GPS coordinates, meeting room name, link to video call.
/// - locations: The locations of the calendar event. e.g. address, GPS coordinates, meeting room name, link to video call.
/// - geohash: The [geohash](https://en.wikipedia.org/wiki/Geohash) to associate calendar event with a searchable physical location.
/// - participants: The participants of the calendar event.
/// - hashtags: Hashtags to categorize the calendar event.
Expand All @@ -351,7 +351,7 @@ public extension EventCreating {
/// - Returns: The signed ``DateBasedCalendarEvent``.
///
/// See [NIP-52](https://github.com/nostr-protocol/nips/blob/master/52.md).
func dateBasedCalendarEvent(withName name: String, description: String = "", startDate: TimeOmittedDate, endDate: TimeOmittedDate? = nil, location: String? = nil, geohash: String? = nil, participants: [CalendarEventParticipant]? = nil, hashtags: [String]? = nil, references: [URL]? = nil, signedBy keypair: Keypair) throws -> DateBasedCalendarEvent {
func dateBasedCalendarEvent(withTitle title: String, description: String = "", startDate: TimeOmittedDate, endDate: TimeOmittedDate? = nil, locations: [String]? = nil, geohash: String? = nil, participants: [CalendarEventParticipant]? = nil, hashtags: [String]? = nil, references: [URL]? = nil, signedBy keypair: Keypair) throws -> DateBasedCalendarEvent {

var tags: [Tag] = []

Expand All @@ -369,12 +369,12 @@ public extension EventCreating {
// and the end date being placed next to the start date.
tags = [
Tag(name: .identifier, value: UUID().uuidString),
Tag(name: "name", value: name),
Tag(name: .title, value: title),
Tag(name: "start", value: startDate.dateString)
] + tags

if let location {
tags.append(Tag(name: "location", value: location))
if let locations, !locations.isEmpty {
tags += locations.map { Tag(name: "location", value: $0) }
}

if let geohash {
Expand All @@ -398,13 +398,13 @@ public extension EventCreating {

/// Creates a ``TimeBasedCalendarEvent`` (kind 31923) which spans between a start time and end time.
/// - Parameters:
/// - name: The name of the calendar event.
/// - title: The title of the calendar event.
/// - description: A detailed description of the calendar event.
/// - startTimestamp: An inclusive start timestamp.
/// - endTimestamp: An exclusive end timestamp. If omitted, the calendar event ends instantaneously.
/// - startTimeZone: The time zone of the start timestamp.
/// - endTimeZone: The time zone of the end timestamp. If omitted and startTimeZone is provided, the time zone of the end timestamp is the same as the start timestamp.
/// - location: The location of the calendar event. e.g. address, GPS coordinates, meeting room name, link to video call.
/// - locations: The locations of the calendar event. e.g. address, GPS coordinates, meeting room name, link to video call.
/// - geohash: The [geohash](https://en.wikipedia.org/wiki/Geohash) to associate calendar event with a searchable physical location.
/// - participants: The participants of the calendar event.
/// - hashtags: Hashtags to categorize the calendar event.
Expand All @@ -413,7 +413,7 @@ public extension EventCreating {
/// - Returns: The signed ``TimeBasedCalendarEvent``.
///
/// See [NIP-52](https://github.com/nostr-protocol/nips/blob/master/52.md).
func timeBasedCalendarEvent(withName name: String, description: String = "", startTimestamp: Date, endTimestamp: Date? = nil, startTimeZone: TimeZone? = nil, endTimeZone: TimeZone? = nil, location: String? = nil, geohash: String? = nil, participants: [CalendarEventParticipant]? = nil, hashtags: [String]? = nil, references: [URL]? = nil, signedBy keypair: Keypair) throws -> TimeBasedCalendarEvent {
func timeBasedCalendarEvent(withTitle title: String, description: String = "", startTimestamp: Date, endTimestamp: Date? = nil, startTimeZone: TimeZone? = nil, endTimeZone: TimeZone? = nil, locations: [String]? = nil, geohash: String? = nil, participants: [CalendarEventParticipant]? = nil, hashtags: [String]? = nil, references: [URL]? = nil, signedBy keypair: Keypair) throws -> TimeBasedCalendarEvent {

// If the end timestamp is omitted, the calendar event ends instantaneously.
if let endTimestamp {
Expand All @@ -425,7 +425,7 @@ public extension EventCreating {

var tags: [Tag] = [
Tag(name: .identifier, value: UUID().uuidString),
Tag(name: "name", value: name),
Tag(name: .title, value: title),
Tag(name: "start", value: String(Int64(startTimestamp.timeIntervalSince1970)))
]

Expand All @@ -442,8 +442,8 @@ public extension EventCreating {
tags.append(Tag(name: "end_tzid", value: endTimeZone.identifier))
}

if let location {
tags.append(Tag(name: "location", value: location))
if let locations, !locations.isEmpty {
tags += locations.map { Tag(name: "location", value: $0) }
}

if let geohash {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,17 @@

import Foundation

public protocol CalendarEventInterpreting: NostrEvent, CalendarEventParticipantInterpreting, HashtagInterpreting, ReferenceTagInterpreting {}
public protocol CalendarEventInterpreting: NostrEvent, CalendarEventParticipantInterpreting, HashtagInterpreting, ReferenceTagInterpreting, IdentifierTagInterpreting, TitleTagInterpreting {}
public extension CalendarEventInterpreting {
/// Universally unique identifier (UUID).
var uuid: String? {
tags.first { $0.name == TagName.identifier.rawValue }?.value
}

/// The name of the calendar event.
@available(*, deprecated, message: "This method of naming a calendar event is out of spec, not preferred, and will be removed in the future. Please use only the title field when it is available.")
var name: String? {
tags.first { $0.name == "name" }?.value
}

/// The location of the calendar event. e.g. address, GPS coordinates, meeting room name, link to video call.
var location: String? {
tags.first { $0.name == "location" }?.value
/// The locations of the calendar event. e.g. address, GPS coordinates, meeting room name, link to video call.
var locations: [String] {
tags.filter { $0.name == "location" }.map { $0.value }
}

/// The [geohash](https://en.wikipedia.org/wiki/Geohash) to associate calendar event with a searchable physical location.
Expand Down
31 changes: 31 additions & 0 deletions Sources/NostrSDK/Events/Calendars/CalendarEventRSVP.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// CalendarEventRSVP.swift
//
//
// Created by Terry Yiu on 12/1/23.
//

import Foundation

public final class CalendarEventRSVP: NostrEvent, IdentifierTagInterpreting {
public var calendarEventIdentifier: String? {
// TODO

Check failure on line 12 in Sources/NostrSDK/Events/Calendars/CalendarEventRSVP.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (todo)
return nil
}

public var status: CalendarEventRSVPStatus? {
guard let statusTag = tags.first(where: { $0.name == "l" && $0.otherParameters.first == "status" }) else {
return nil
}

return CalendarEventRSVPStatus(rawValue: statusTag.value)
}

public var freebusy: CalendarEventRSVPFreebusy? {
guard let freebusyTag = tags.first(where: { $0.name == "l" && $0.otherParameters.first == "freebusy" }) else {
return nil
}

return CalendarEventRSVPFreebusy(rawValue: freebusyTag.value)
}
}
35 changes: 35 additions & 0 deletions Sources/NostrSDK/Events/Calendars/CalendarEventRSVPFreebusy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// CalendarEventRSVPFreebusy.swift
//
//
// Created by Terry Yiu on 12/17/23.
//

import Foundation

public enum CalendarEventRSVPFreebusy: RawRepresentable, CaseIterable, Codable, Equatable {

public typealias RawValue = String

case free
case busy
case unknown(RawValue)

static public let allCases: AllCases = [
.free,
.busy
]

public init?(rawValue: String) {
self = Self.allCases.first { $0.rawValue == rawValue }
?? .unknown(rawValue)
}

public var rawValue: String {
switch self {
case .free: return "free"
case .busy: return "busy"
case let .unknown(value): return value
}
}
}
38 changes: 38 additions & 0 deletions Sources/NostrSDK/Events/Calendars/CalendarEventRSVPStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// File.swift
//
//
// Created by Terry Yiu on 12/17/23.
//

import Foundation

public enum CalendarEventRSVPStatus: RawRepresentable, CaseIterable, Codable, Equatable {

public typealias RawValue = String

case accepted
case declined
case tentative
case unknown(RawValue)

static public let allCases: AllCases = [
.accepted,
.declined,
.tentative
]

public init?(rawValue: String) {
self = Self.allCases.first { $0.rawValue == rawValue }
?? .unknown(rawValue)
}

public var rawValue: String {
switch self {
case .accepted: return "accepted"
case .declined: return "declined"
case .tentative: return "tentative"
case let .unknown(value): return value
}
}
}
11 changes: 11 additions & 0 deletions Sources/NostrSDK/Events/Calendars/CalendarNostrEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// CalendarNostrEvent.swift
//
//
// Created by Terry Yiu on 12/1/23.
//

import Foundation

public final class CalendarNostrEvent: NostrEvent, IdentifierTagInterpreting, TitleTagInterpreting {
}
17 changes: 17 additions & 0 deletions Sources/NostrSDK/Events/Tags/IdentifierTagInterpreting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// IdentifierTagInterpreting.swift
//
//
// Created by Terry Yiu on 12/8/23.
//

import Foundation

public protocol IdentifierTagInterpreting: NostrEvent {}
public extension IdentifierTagInterpreting {
/// The identifier of the event. For parameterized replaceable events, this identifier remains stable across replacements.
/// This identifier is represented by the ``d`` tag, which is distinctly different from the ``id`` field on ``NostrEvent``.
var identifier: String? {
firstValueForTagName(.identifier)
}
}
16 changes: 16 additions & 0 deletions Sources/NostrSDK/Events/Tags/TitleTagInterpreting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// TitleTagInterpreting.swift
//
//
// Created by Terry Yiu on 12/8/23.
//

import Foundation

public protocol TitleTagInterpreting: NostrEvent {}
public extension TitleTagInterpreting {
/// The title of the event.
var title: String? {
firstValueForTagName(.title)
}
}
2 changes: 1 addition & 1 deletion Sources/NostrSDK/Tag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public enum TagName: String {

case summary

/// a title for a long-form content event
/// a title for an event
case title

/// a web URL the event is referring to in some way. See [NIP-24 - Extra metadata fields and tags](https://github.com/nostr-protocol/nips/blob/master/24.md#tags).
Expand Down
Loading

0 comments on commit 08165aa

Please sign in to comment.