9fd04b0ce4
Add the owner-only AF_UNIX Reyna CLI privacy host, strict signed-app installation, and typed native routing for Calendar, Contacts, and Reminders.\n\nAdd bounded system-status paths and config-only direct local-service wrappers. Preserve MacMiniMCP pending explicit cutover approval.\n\nApple Notes is intentionally deferred: no native Notes operations, Apple Events declaration, or Automation helper are included; legacy Notes handling remains untouched.
1084 lines
50 KiB
Swift
1084 lines
50 KiB
Swift
import Foundation
|
|
|
|
// MARK: - JSONValue safe representation
|
|
enum JSONValue: Codable, Equatable, Sendable {
|
|
case object([String: JSONValue])
|
|
case array([JSONValue])
|
|
case string(String)
|
|
case bool(Bool)
|
|
case number(Double)
|
|
case null
|
|
|
|
init() {
|
|
self = .object([:])
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
if container.decodeNil() {
|
|
self = .null
|
|
return
|
|
}
|
|
if let b = try? container.decode(Bool.self) {
|
|
self = .bool(b)
|
|
return
|
|
}
|
|
if let obj = try? container.decode([String: JSONValue].self) {
|
|
self = .object(obj)
|
|
return
|
|
}
|
|
if let arr = try? container.decode([JSONValue].self) {
|
|
self = .array(arr)
|
|
return
|
|
}
|
|
if let s = try? container.decode(String.self) {
|
|
self = .string(s)
|
|
return
|
|
}
|
|
if let intVal = try? container.decode(Int.self) {
|
|
self = .number(Double(intVal))
|
|
return
|
|
}
|
|
if let doubleVal = try? container.decode(Double.self) {
|
|
self = .number(doubleVal)
|
|
return
|
|
}
|
|
throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unrecognized JSON value"))
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
switch self {
|
|
case .null:
|
|
try container.encodeNil()
|
|
case .bool(let b):
|
|
try container.encode(b)
|
|
case .string(let s):
|
|
try container.encode(s)
|
|
case .number(let d):
|
|
if d.truncatingRemainder(dividingBy: 1) == 0, d >= Double(Int.min), d <= Double(Int.max) {
|
|
try container.encode(Int(d))
|
|
} else {
|
|
try container.encode(d)
|
|
}
|
|
case .object(let o):
|
|
try container.encode(o)
|
|
case .array(let a):
|
|
try container.encode(a)
|
|
}
|
|
}
|
|
}
|
|
|
|
typealias Args = JSONValue
|
|
|
|
struct CalendarListItem: Codable, Equatable, Sendable {
|
|
let id: String
|
|
let title: String
|
|
let source: String
|
|
let type: String
|
|
}
|
|
|
|
struct CalendarEventItem: Codable, Equatable, Sendable {
|
|
let id: String
|
|
let title: String
|
|
let start: String
|
|
let end: String
|
|
let all_day: Bool
|
|
let calendar_id: String
|
|
let calendar_title: String
|
|
let notes: String?
|
|
let location: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case title
|
|
case start
|
|
case end
|
|
case all_day
|
|
case calendar_id
|
|
case calendar_title
|
|
case notes
|
|
case location
|
|
}
|
|
}
|
|
|
|
enum CalendarProviderError: Error, Equatable, Sendable {
|
|
case permissionRequired
|
|
case unavailable(String)
|
|
case invalidRequest(String)
|
|
|
|
var isPermission: Bool {
|
|
if case .permissionRequired = self { return true }
|
|
return false
|
|
}
|
|
}
|
|
|
|
protocol CalendarListProviding: Sendable {
|
|
func listCalendars() throws -> [CalendarListItem]
|
|
}
|
|
|
|
protocol CalendarEventsListProviding: Sendable {
|
|
func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem]
|
|
}
|
|
|
|
protocol CalendarEventCreateProviding: Sendable {
|
|
func createEvent(
|
|
title: String,
|
|
start: Date,
|
|
end: Date,
|
|
allDay: Bool,
|
|
notes: String?,
|
|
location: String?,
|
|
calendarId: String?,
|
|
calendarTitle: String?
|
|
) throws -> CalendarEventItem
|
|
}
|
|
|
|
protocol FullCalendarProviding: CalendarListProviding, CalendarEventsListProviding, CalendarEventCreateProviding {}
|
|
|
|
struct DefaultCalendarListProvider: CalendarListProviding {
|
|
func listCalendars() throws -> [CalendarListItem] {
|
|
throw CalendarProviderError.unavailable("no provider configured")
|
|
}
|
|
}
|
|
|
|
struct Request: Codable, Sendable {
|
|
let id: String
|
|
let operation: String
|
|
let arguments: JSONValue
|
|
}
|
|
|
|
struct ResultPayload: Codable, Equatable, Sendable {
|
|
let protocol_version: String
|
|
let operation: String
|
|
let status: String?
|
|
let calendars: [CalendarListItem]?
|
|
let events: [CalendarEventItem]?
|
|
let event: CalendarEventItem?
|
|
let contacts: [ContactListItem]?
|
|
let contact: ContactDetailItem?
|
|
let created_contact: ContactCreateResult?
|
|
let reminder_lists: [ReminderListItem]?
|
|
let reminders: [ReminderItem]?
|
|
let reminder: ReminderItem?
|
|
let created_reminder: ReminderCreateResult?
|
|
let system_info: SystemInfoItem?
|
|
let speech_api_status: SpeechApiStatusItem?
|
|
|
|
init(
|
|
protocol_version: String,
|
|
operation: String,
|
|
status: String? = nil,
|
|
calendars: [CalendarListItem]? = nil,
|
|
events: [CalendarEventItem]? = nil,
|
|
event: CalendarEventItem? = nil,
|
|
contacts: [ContactListItem]? = nil,
|
|
contact: ContactDetailItem? = nil,
|
|
created_contact: ContactCreateResult? = nil,
|
|
reminder_lists: [ReminderListItem]? = nil,
|
|
reminders: [ReminderItem]? = nil,
|
|
reminder: ReminderItem? = nil,
|
|
created_reminder: ReminderCreateResult? = nil,
|
|
system_info: SystemInfoItem? = nil,
|
|
speech_api_status: SpeechApiStatusItem? = nil
|
|
) {
|
|
self.protocol_version = protocol_version
|
|
self.operation = operation
|
|
self.status = status
|
|
self.calendars = calendars
|
|
self.events = events
|
|
self.event = event
|
|
self.contacts = contacts
|
|
self.contact = contact
|
|
self.created_contact = created_contact
|
|
self.reminder_lists = reminder_lists
|
|
self.reminders = reminders
|
|
self.reminder = reminder
|
|
self.created_reminder = created_reminder
|
|
self.system_info = system_info
|
|
self.speech_api_status = speech_api_status
|
|
}
|
|
}
|
|
|
|
struct ErrorPayload: Codable, Equatable, Sendable {
|
|
let code: String
|
|
let message: String
|
|
}
|
|
|
|
struct Response: Codable, Equatable, Sendable {
|
|
let id: String
|
|
let ok: Bool
|
|
let result: ResultPayload?
|
|
let error: ErrorPayload?
|
|
}
|
|
|
|
let PROTOCOL_VERSION = "1.0.0"
|
|
|
|
// MARK: - Shared ISO8601 parsing & validation helpers
|
|
enum CalendarArgsValidationError: Error, Equatable, Sendable {
|
|
case invalid(String)
|
|
}
|
|
|
|
nonisolated(unsafe) private let iso8601WithFractional: ISO8601DateFormatter = {
|
|
let f = ISO8601DateFormatter()
|
|
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
return f
|
|
}()
|
|
|
|
nonisolated(unsafe) private let iso8601Plain: ISO8601DateFormatter = {
|
|
let f = ISO8601DateFormatter()
|
|
f.formatOptions = [.withInternetDateTime]
|
|
return f
|
|
}()
|
|
|
|
func parseISO8601DateStrict(_ s: String) throws -> Date {
|
|
if let d = iso8601WithFractional.date(from: s) { return d }
|
|
if let d = iso8601Plain.date(from: s) { return d }
|
|
throw CalendarProviderError.invalidRequest("Invalid ISO-8601 date: \(s)")
|
|
}
|
|
|
|
func extractStringArg(_ args: JSONValue, key: String) -> String? {
|
|
guard case .object(let obj) = args, let val = obj[key] else { return nil }
|
|
switch val {
|
|
case .string(let s): return s
|
|
case .null: return nil
|
|
default: return nil
|
|
}
|
|
}
|
|
|
|
func extractBoolArg(_ args: JSONValue, key: String, defaultValue: Bool) -> Bool {
|
|
guard case .object(let obj) = args, let val = obj[key] else { return defaultValue }
|
|
switch val {
|
|
case .bool(let b): return b
|
|
case .object(_), .array(_), .string(_), .number(_), .null: return defaultValue
|
|
}
|
|
}
|
|
|
|
func extractIntArg(_ args: JSONValue, key: String) -> Int? {
|
|
guard case .object(let obj) = args, let val = obj[key] else { return nil }
|
|
switch val {
|
|
case .number(let n): return Int(n)
|
|
case .string(let s): return Int(s)
|
|
default: return nil
|
|
}
|
|
}
|
|
|
|
func iso8601StringFromDate(_ d: Date) -> String {
|
|
let f = ISO8601DateFormatter()
|
|
f.formatOptions = [.withInternetDateTime]
|
|
return f.string(from: d)
|
|
}
|
|
|
|
func dispatch(
|
|
request: Request,
|
|
calendarProvider: CalendarListProviding = ProductionCalendarProvider(),
|
|
authProvider: CalendarAuthorizationProviding = ProductionAuthProvider()
|
|
) -> Response {
|
|
return dispatch(
|
|
request: request,
|
|
calendarProvider: calendarProvider,
|
|
eventsProvider: ProductionCalendarEventsProvider(),
|
|
createProvider: ProductionCalendarEventCreateProvider(),
|
|
authProvider: authProvider,
|
|
contactsAuthProvider: ProductionContactsAuthProvider(),
|
|
contactsSearchProvider: ProductionContactsSearchProvider(),
|
|
contactsReadProvider: ProductionContactsReadProvider(),
|
|
contactsCreateProvider: ProductionContactsCreateProvider(),
|
|
remindersAuthProvider: ProductionRemindersAuthProvider(),
|
|
remindersListsProvider: ProductionRemindersListsProvider(),
|
|
remindersListProvider: ProductionRemindersListProvider(),
|
|
remindersCreateProvider: ProductionRemindersCreateProvider()
|
|
)
|
|
}
|
|
|
|
func dispatch(
|
|
request: Request,
|
|
calendarProvider: CalendarListProviding,
|
|
eventsProvider: CalendarEventsListProviding,
|
|
createProvider: CalendarEventCreateProviding,
|
|
authProvider: CalendarAuthorizationProviding,
|
|
contactsAuthProvider: ContactsAuthorizationProviding = ProductionContactsAuthProvider(),
|
|
contactsSearchProvider: ContactsSearchProviding = ProductionContactsSearchProvider(),
|
|
contactsReadProvider: ContactsReadProviding = ProductionContactsReadProvider(),
|
|
contactsCreateProvider: ContactsCreateProviding = ProductionContactsCreateProvider(),
|
|
remindersAuthProvider: RemindersAuthorizationProviding = ProductionRemindersAuthProvider(),
|
|
remindersListsProvider: RemindersListsProviding = ProductionRemindersListsProvider(),
|
|
remindersListProvider: RemindersListProviding = ProductionRemindersListProvider(),
|
|
remindersCreateProvider: RemindersCreateProviding = ProductionRemindersCreateProvider()
|
|
) -> Response {
|
|
switch request.operation {
|
|
case "service.health":
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "service.health",
|
|
status: "ok",
|
|
calendars: nil
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
|
|
case "calendar.list":
|
|
do {
|
|
let items = try calendarProvider.listCalendars()
|
|
let sorted = items.sorted {
|
|
if $0.source != $1.source { return $0.source < $1.source }
|
|
if $0.title != $1.title { return $0.title < $1.title }
|
|
return $0.id < $1.id
|
|
}
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "calendar.list",
|
|
status: "ok",
|
|
calendars: sorted
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} catch let err as CalendarProviderError {
|
|
switch err {
|
|
case .permissionRequired:
|
|
let e = ErrorPayload(code: "permission_required", message: "Calendar access not authorized. Grant permission in System Settings.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .unavailable(let msg):
|
|
let e = ErrorPayload(code: "calendar_unavailable", message: msg.isEmpty ? "Calendar unavailable" : msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .invalidRequest(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
} catch {
|
|
let e = ErrorPayload(code: "calendar_unavailable", message: "Calendar unavailable")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
case "calendar.request_full_access":
|
|
do {
|
|
let status = authProvider.authorizationStatus()
|
|
if status == .authorized {
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "calendar.request_full_access",
|
|
status: "authorized",
|
|
calendars: nil
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
}
|
|
let granted: Bool
|
|
do {
|
|
granted = try authProvider.requestFullAccess()
|
|
} catch let err as CalendarProviderError {
|
|
switch err {
|
|
case .unavailable(let msg):
|
|
let e = ErrorPayload(code: "calendar_unavailable", message: msg.isEmpty ? "Calendar unavailable" : msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .permissionRequired:
|
|
let e = ErrorPayload(code: "permission_denied", message: "Calendar access denied.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .invalidRequest(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
} catch {
|
|
let e = ErrorPayload(code: "calendar_unavailable", message: "Calendar unavailable")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
if granted {
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "calendar.request_full_access",
|
|
status: "authorized",
|
|
calendars: nil
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} else {
|
|
let e = ErrorPayload(code: "permission_denied", message: "Calendar access denied. Grant permission in System Settings > Privacy & Security > Calendars.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
}
|
|
|
|
case "calendar.events.list":
|
|
do {
|
|
guard let startStr = extractStringArg(request.arguments, key: "start"), !startStr.isEmpty else {
|
|
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: start")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
guard let endStr = extractStringArg(request.arguments, key: "end"), !endStr.isEmpty else {
|
|
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: end")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
let startDate: Date
|
|
let endDate: Date
|
|
do {
|
|
startDate = try parseISO8601DateStrict(startStr)
|
|
} catch let err as CalendarProviderError {
|
|
if case .invalidRequest(let msg) = err {
|
|
return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg))
|
|
}
|
|
throw err
|
|
}
|
|
do {
|
|
endDate = try parseISO8601DateStrict(endStr)
|
|
} catch let err as CalendarProviderError {
|
|
if case .invalidRequest(let msg) = err {
|
|
return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg))
|
|
}
|
|
throw err
|
|
}
|
|
|
|
if startDate >= endDate {
|
|
let e = ErrorPayload(code: "invalid_request", message: "start must occur before end")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
var limit = 50
|
|
if let extracted = extractIntArg(request.arguments, key: "limit") {
|
|
limit = extracted
|
|
}
|
|
if limit < 1 || limit > 250 {
|
|
let e = ErrorPayload(code: "invalid_request", message: "limit must be between 1 and 250")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
let calId = extractStringArg(request.arguments, key: "calendar_id")
|
|
?? extractStringArg(request.arguments, key: "calendarId")
|
|
let calTitle = extractStringArg(request.arguments, key: "calendar")
|
|
?? extractStringArg(request.arguments, key: "calendar_title")
|
|
?? extractStringArg(request.arguments, key: "calendarTitle")
|
|
|
|
let items = try eventsProvider.listEvents(start: startDate, end: endDate, calendarId: calId, calendarTitle: calTitle, limit: limit)
|
|
|
|
let sorted = items.sorted {
|
|
if $0.start != $1.start { return $0.start < $1.start }
|
|
if $0.title != $1.title { return $0.title < $1.title }
|
|
return $0.id < $1.id
|
|
}
|
|
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "calendar.events.list",
|
|
status: "ok",
|
|
events: sorted
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} catch let err as CalendarProviderError {
|
|
switch err {
|
|
case .permissionRequired:
|
|
let e = ErrorPayload(code: "permission_required", message: "Calendar access not authorized. Grant permission in System Settings.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .unavailable(let msg):
|
|
let e = ErrorPayload(code: "calendar_unavailable", message: msg.isEmpty ? "Calendar unavailable" : msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .invalidRequest(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
} catch {
|
|
let e = ErrorPayload(code: "calendar_unavailable", message: "Calendar unavailable")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
case "calendar.event.create":
|
|
do {
|
|
guard let title = extractStringArg(request.arguments, key: "title"), !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: title")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
guard let startStr = extractStringArg(request.arguments, key: "start"), !startStr.isEmpty else {
|
|
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: start")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
guard let endStr = extractStringArg(request.arguments, key: "end"), !endStr.isEmpty else {
|
|
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: end")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
let startDate: Date
|
|
let endDate: Date
|
|
do { startDate = try parseISO8601DateStrict(startStr) } catch let err as CalendarProviderError {
|
|
if case .invalidRequest(let msg) = err { return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg)) }
|
|
throw err
|
|
}
|
|
do { endDate = try parseISO8601DateStrict(endStr) } catch let err as CalendarProviderError {
|
|
if case .invalidRequest(let msg) = err { return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg)) }
|
|
throw err
|
|
}
|
|
|
|
if startDate >= endDate {
|
|
let e = ErrorPayload(code: "invalid_request", message: "start must occur before end")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
let allDay = extractBoolArg(request.arguments, key: "all_day", defaultValue: extractBoolArg(request.arguments, key: "allDay", defaultValue: false))
|
|
let notes = extractStringArg(request.arguments, key: "notes")
|
|
let location = extractStringArg(request.arguments, key: "location")
|
|
let calId = extractStringArg(request.arguments, key: "calendar_id") ?? extractStringArg(request.arguments, key: "calendarId")
|
|
let calTitle = extractStringArg(request.arguments, key: "calendar") ?? extractStringArg(request.arguments, key: "calendar_title") ?? extractStringArg(request.arguments, key: "calendarTitle")
|
|
|
|
let created = try createProvider.createEvent(
|
|
title: title,
|
|
start: startDate,
|
|
end: endDate,
|
|
allDay: allDay,
|
|
notes: notes,
|
|
location: location,
|
|
calendarId: calId,
|
|
calendarTitle: calTitle
|
|
)
|
|
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "calendar.event.create",
|
|
status: "ok",
|
|
event: created
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} catch let err as CalendarProviderError {
|
|
switch err {
|
|
case .permissionRequired:
|
|
let e = ErrorPayload(code: "permission_required", message: "Calendar access not authorized. Grant permission in System Settings.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .unavailable(let msg):
|
|
let e = ErrorPayload(code: "calendar_unavailable", message: msg.isEmpty ? "Calendar unavailable" : msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .invalidRequest(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
} catch {
|
|
let e = ErrorPayload(code: "calendar_unavailable", message: "Calendar unavailable")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
case "contacts.request_access":
|
|
do {
|
|
let status = contactsAuthProvider.authorizationStatus()
|
|
if status == .authorized {
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "contacts.request_access",
|
|
status: "authorized"
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
}
|
|
let granted: Bool
|
|
do {
|
|
granted = try contactsAuthProvider.requestAccess()
|
|
} catch let err as ContactsProviderError {
|
|
switch err {
|
|
case .unavailable(let msg):
|
|
let e = ErrorPayload(code: "contacts_unavailable", message: msg.isEmpty ? "Contacts unavailable" : msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .permissionRequired, .permissionDenied:
|
|
let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .invalidRequest(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .notFound(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
} catch {
|
|
let e = ErrorPayload(code: "contacts_unavailable", message: "Contacts unavailable")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
if granted {
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "contacts.request_access",
|
|
status: "authorized"
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} else {
|
|
let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied. Grant permission in System Settings > Privacy & Security > Contacts.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
}
|
|
|
|
case "contacts.search":
|
|
do {
|
|
var limit = 50
|
|
if let extracted = extractIntArg(request.arguments, key: "limit") {
|
|
limit = extracted
|
|
}
|
|
if limit < 1 || limit > 100 {
|
|
let e = ErrorPayload(code: "invalid_request", message: "limit must be between 1 and 100")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
let queryObj: String?
|
|
if case .object(let obj) = request.arguments {
|
|
if let q = obj["query"] {
|
|
switch q {
|
|
case .string(let s): queryObj = s
|
|
case .null: queryObj = nil
|
|
default: queryObj = nil
|
|
}
|
|
} else {
|
|
queryObj = nil
|
|
}
|
|
} else {
|
|
queryObj = nil
|
|
}
|
|
// Also accept empty string? legacy allowed query nil
|
|
let items = try contactsSearchProvider.searchContacts(query: queryObj, limit: limit)
|
|
let sorted = items.sorted {
|
|
if $0.name != $1.name { return $0.name < $1.name }
|
|
if $0.organization != $1.organization { return $0.organization < $1.organization }
|
|
return $0.id < $1.id
|
|
}
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "contacts.search",
|
|
status: "ok",
|
|
contacts: sorted
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} catch let err as ContactsProviderError {
|
|
switch err {
|
|
case .permissionRequired:
|
|
let e = ErrorPayload(code: "permission_required", message: "Contacts access not authorized. Grant permission in System Settings.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .permissionDenied:
|
|
let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .unavailable(let msg):
|
|
let e = ErrorPayload(code: "contacts_unavailable", message: msg.isEmpty ? "Contacts unavailable" : msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .invalidRequest(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .notFound(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
} catch {
|
|
let e = ErrorPayload(code: "contacts_unavailable", message: "Contacts unavailable")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
case "contacts.read":
|
|
do {
|
|
guard let idVal = extractStringArg(request.arguments, key: "id"), !idVal.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: id")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
let item = try contactsReadProvider.readContact(id: idVal)
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "contacts.read",
|
|
status: "ok",
|
|
contact: item
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} catch let err as ContactsProviderError {
|
|
switch err {
|
|
case .permissionRequired:
|
|
let e = ErrorPayload(code: "permission_required", message: "Contacts access not authorized. Grant permission in System Settings.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .permissionDenied:
|
|
let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .unavailable(let msg):
|
|
let e = ErrorPayload(code: "contacts_unavailable", message: msg.isEmpty ? "Contacts unavailable" : msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .invalidRequest(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .notFound(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
} catch {
|
|
let e = ErrorPayload(code: "contacts_unavailable", message: "Contacts unavailable")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
case "contacts.create":
|
|
do {
|
|
// Legacy keys: firstName/lastName/organization/jobTitle/note + email/phone objects
|
|
var firstName: String? = extractStringArg(request.arguments, key: "firstName") ?? extractStringArg(request.arguments, key: "first_name")
|
|
var lastName: String? = extractStringArg(request.arguments, key: "lastName") ?? extractStringArg(request.arguments, key: "last_name")
|
|
var organization: String? = extractStringArg(request.arguments, key: "organization")
|
|
var jobTitle: String? = extractStringArg(request.arguments, key: "jobTitle") ?? extractStringArg(request.arguments, key: "job_title")
|
|
var note: String? = extractStringArg(request.arguments, key: "note")
|
|
|
|
// At least one of firstName/lastName/organization required to create meaningful contact
|
|
let hasName = !(firstName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true) || !(lastName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true) || !(organization?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
|
|
if !hasName {
|
|
// allow empty still? per legacy it allowed empty, but we require at least something
|
|
// However preserve compat: if nothing, invalid_request
|
|
let e = ErrorPayload(code: "invalid_request", message: "Missing required field: at least firstName, lastName, or organization required")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
var emailLV: ContactEmailLabelValue? = nil
|
|
var phoneLV: ContactPhoneLabelValue? = nil
|
|
|
|
if case .object(let outer) = request.arguments {
|
|
if let ev = outer["email"] {
|
|
if case .object(let eObj) = ev {
|
|
let label: String
|
|
let value: String
|
|
switch eObj["label"] {
|
|
case .string(let s): label = s
|
|
default: label = "work"
|
|
}
|
|
switch eObj["value"] {
|
|
case .string(let s): value = s
|
|
default: value = ""
|
|
}
|
|
if !value.isEmpty {
|
|
emailLV = ContactEmailLabelValue(label: label, value: value)
|
|
}
|
|
}
|
|
}
|
|
if let pv = outer["phone"] {
|
|
if case .object(let pObj) = pv {
|
|
let label: String
|
|
let value: String
|
|
switch pObj["label"] {
|
|
case .string(let s): label = s
|
|
default: label = "mobile"
|
|
}
|
|
switch pObj["value"] {
|
|
case .string(let s): value = s
|
|
default: value = ""
|
|
}
|
|
if !value.isEmpty {
|
|
phoneLV = ContactPhoneLabelValue(label: label, value: value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let created = try contactsCreateProvider.createContact(
|
|
firstName: firstName,
|
|
lastName: lastName,
|
|
organization: organization,
|
|
jobTitle: jobTitle,
|
|
note: note,
|
|
email: emailLV,
|
|
phone: phoneLV
|
|
)
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "contacts.create",
|
|
status: "ok",
|
|
created_contact: created
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} catch let err as ContactsProviderError {
|
|
switch err {
|
|
case .permissionRequired:
|
|
let e = ErrorPayload(code: "permission_required", message: "Contacts access not authorized. Grant permission in System Settings.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .permissionDenied:
|
|
let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .unavailable(let msg):
|
|
let e = ErrorPayload(code: "contacts_unavailable", message: msg.isEmpty ? "Contacts unavailable" : msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .invalidRequest(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .notFound(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
} catch {
|
|
let e = ErrorPayload(code: "contacts_unavailable", message: "Contacts unavailable")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
case "reminders.request_full_access":
|
|
do {
|
|
let status = remindersAuthProvider.authorizationStatus()
|
|
if status == .authorized {
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "reminders.request_full_access",
|
|
status: "authorized"
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
}
|
|
let granted: Bool
|
|
do {
|
|
granted = try remindersAuthProvider.requestFullAccess()
|
|
} catch let err as RemindersProviderError {
|
|
switch err {
|
|
case .unavailable(let msg):
|
|
let e = ErrorPayload(code: "reminders_unavailable", message: msg.isEmpty ? "Reminders unavailable" : msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .permissionRequired, .permissionDenied:
|
|
let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .invalidRequest(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
} catch {
|
|
let e = ErrorPayload(code: "reminders_unavailable", message: "Reminders unavailable")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
if granted {
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "reminders.request_full_access",
|
|
status: "authorized"
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} else {
|
|
let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied. Grant permission in System Settings > Privacy & Security > Reminders.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
}
|
|
|
|
case "reminders.lists":
|
|
do {
|
|
let items = try remindersListsProvider.listReminderLists()
|
|
let sorted = items.sorted {
|
|
if $0.source != $1.source { return $0.source < $1.source }
|
|
if $0.title != $1.title { return $0.title < $1.title }
|
|
return $0.id < $1.id
|
|
}
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "reminders.lists",
|
|
status: "ok",
|
|
reminder_lists: sorted
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} catch let err as RemindersProviderError {
|
|
switch err {
|
|
case .permissionRequired:
|
|
let e = ErrorPayload(code: "permission_required", message: "Reminders access not authorized. Grant permission in System Settings.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .permissionDenied:
|
|
let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .unavailable(let msg):
|
|
let e = ErrorPayload(code: "reminders_unavailable", message: msg.isEmpty ? "Reminders unavailable" : msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .invalidRequest(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
} catch {
|
|
let e = ErrorPayload(code: "reminders_unavailable", message: "Reminders unavailable")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
case "reminders.list":
|
|
do {
|
|
var limit = 50
|
|
if let extracted = extractIntArg(request.arguments, key: "limit") {
|
|
limit = extracted
|
|
}
|
|
if limit < 1 || limit > 250 {
|
|
let e = ErrorPayload(code: "invalid_request", message: "limit must be between 1 and 250")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
let listId = extractStringArg(request.arguments, key: "list_id") ?? extractStringArg(request.arguments, key: "listId") ?? extractStringArg(request.arguments, key: "id")
|
|
let listTitle = extractStringArg(request.arguments, key: "list") ?? extractStringArg(request.arguments, key: "list_title") ?? extractStringArg(request.arguments, key: "listTitle")
|
|
|
|
var completedFilter: Bool? = nil
|
|
if case .object(let obj) = request.arguments {
|
|
if let compVal = obj["completed"] {
|
|
switch compVal {
|
|
case .bool(let b): completedFilter = b
|
|
case .null: completedFilter = nil
|
|
default: break
|
|
}
|
|
}
|
|
}
|
|
|
|
let items = try remindersListProvider.listReminders(listId: listId, listTitle: listTitle, completed: completedFilter, limit: limit)
|
|
let sorted = items.sorted {
|
|
let due0 = $0.due ?? ""
|
|
let due1 = $1.due ?? ""
|
|
if due0 != due1 { return due0 < due1 }
|
|
if $0.title != $1.title { return $0.title < $1.title }
|
|
return $0.id < $1.id
|
|
}
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "reminders.list",
|
|
status: "ok",
|
|
reminders: sorted
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} catch let err as RemindersProviderError {
|
|
switch err {
|
|
case .permissionRequired:
|
|
let e = ErrorPayload(code: "permission_required", message: "Reminders access not authorized. Grant permission in System Settings.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .permissionDenied:
|
|
let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .unavailable(let msg):
|
|
let e = ErrorPayload(code: "reminders_unavailable", message: msg.isEmpty ? "Reminders unavailable" : msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .invalidRequest(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
} catch {
|
|
let e = ErrorPayload(code: "reminders_unavailable", message: "Reminders unavailable")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
case "reminders.create":
|
|
do {
|
|
guard let title = extractStringArg(request.arguments, key: "title"), !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: title")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
if title.count > 1024 {
|
|
let e = ErrorPayload(code: "invalid_request", message: "title exceeds maximum length 1024")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
let notes = extractStringArg(request.arguments, key: "notes") ?? extractStringArg(request.arguments, key: "body")
|
|
if let n = notes, n.count > 4096 {
|
|
let e = ErrorPayload(code: "invalid_request", message: "notes exceeds maximum length 4096")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
var dueDate: Date? = nil
|
|
if let dueStr = extractStringArg(request.arguments, key: "due") ?? extractStringArg(request.arguments, key: "dueDate") ?? extractStringArg(request.arguments, key: "due_date") {
|
|
if !dueStr.isEmpty {
|
|
do {
|
|
dueDate = try parseISO8601DateStrict(dueStr)
|
|
} catch let err as CalendarProviderError {
|
|
if case .invalidRequest(let msg) = err {
|
|
return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg))
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
var priority: Int? = nil
|
|
if let p = extractIntArg(request.arguments, key: "priority") {
|
|
if p < 0 || p > 9 {
|
|
let e = ErrorPayload(code: "invalid_request", message: "priority must be between 0 and 9")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
priority = p
|
|
}
|
|
|
|
let listId = extractStringArg(request.arguments, key: "list_id") ?? extractStringArg(request.arguments, key: "listId")
|
|
let listTitle = extractStringArg(request.arguments, key: "list") ?? extractStringArg(request.arguments, key: "list_title") ?? extractStringArg(request.arguments, key: "listTitle")
|
|
|
|
if (listId == nil || listId?.isEmpty == true) && (listTitle == nil || listTitle?.isEmpty == true) {
|
|
let e = ErrorPayload(code: "invalid_request", message: "Reminders list must be specified by id or exact unique title")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
let created = try remindersCreateProvider.createReminder(title: title, listId: listId, listTitle: listTitle, notes: notes, due: dueDate, priority: priority)
|
|
let result = ResultPayload(
|
|
protocol_version: PROTOCOL_VERSION,
|
|
operation: "reminders.create",
|
|
status: "ok",
|
|
created_reminder: created
|
|
)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} catch let err as RemindersProviderError {
|
|
switch err {
|
|
case .permissionRequired:
|
|
let e = ErrorPayload(code: "permission_required", message: "Reminders access not authorized. Grant permission in System Settings.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .permissionDenied:
|
|
let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied.")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .unavailable(let msg):
|
|
let e = ErrorPayload(code: "reminders_unavailable", message: msg.isEmpty ? "Reminders unavailable" : msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
case .invalidRequest(let msg):
|
|
let e = ErrorPayload(code: "invalid_request", message: msg)
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
} catch {
|
|
let e = ErrorPayload(code: "reminders_unavailable", message: "Reminders unavailable")
|
|
return Response(id: request.id, ok: false, result: nil, error: e)
|
|
}
|
|
|
|
case "system.get_info":
|
|
do {
|
|
let info = try ProductionSystemInfoProvider().getSystemInfo()
|
|
let result = ResultPayload(protocol_version: PROTOCOL_VERSION, operation: "system.get_info", status: "ok", system_info: info)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} catch {
|
|
return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "system_unavailable", message: "System info unavailable"))
|
|
}
|
|
|
|
case "system.speech_api_status":
|
|
do {
|
|
let st = try ProductionSystemInfoProvider().getSpeechApiStatus()
|
|
let result = ResultPayload(protocol_version: PROTOCOL_VERSION, operation: "system.speech_api_status", status: "ok", speech_api_status: st)
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
} catch {
|
|
return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "system_unavailable", message: "Speech API status unavailable"))
|
|
}
|
|
|
|
case "apple_llm.check":
|
|
// Minimal health probe — returns status ok with notes that full ANE session is owned separately
|
|
let result = ResultPayload(protocol_version: PROTOCOL_VERSION, operation: "apple_llm.check", status: "ok")
|
|
return Response(id: request.id, ok: true, result: result, error: nil)
|
|
|
|
default:
|
|
let err = ErrorPayload(
|
|
code: "unknown_operation",
|
|
message: "Unknown operation: \(request.operation)"
|
|
)
|
|
return Response(id: request.id, ok: false, result: nil, error: err)
|
|
}
|
|
}
|
|
|
|
func dispatch(request: Request) -> Response {
|
|
return dispatch(request: request, calendarProvider: ProductionCalendarProvider(), authProvider: ProductionAuthProvider())
|
|
}
|
|
|
|
func ProductionCalendarProvider() -> CalendarListProviding {
|
|
return EventKitCalendarProvider()
|
|
}
|
|
|
|
func ProductionAuthProvider() -> CalendarAuthorizationProviding {
|
|
return EventKitCalendarAuthorizationProvider()
|
|
}
|
|
|
|
func ProductionCalendarEventsProvider() -> CalendarEventsListProviding {
|
|
return EventKitCalendarProvider()
|
|
}
|
|
|
|
func ProductionCalendarEventCreateProvider() -> CalendarEventCreateProviding {
|
|
return EventKitCalendarProvider()
|
|
}
|
|
|
|
func ProductionContactsAuthProvider() -> ContactsAuthorizationProviding {
|
|
return ContactsAuthorizationProvider()
|
|
}
|
|
|
|
func ProductionContactsSearchProvider() -> ContactsSearchProviding {
|
|
return ContactsSearchProvider()
|
|
}
|
|
|
|
func ProductionContactsReadProvider() -> ContactsReadProviding {
|
|
return ContactsReadProvider()
|
|
}
|
|
|
|
func ProductionContactsCreateProvider() -> ContactsCreateProviding {
|
|
return ContactsCreateProvider()
|
|
}
|
|
|
|
func ProductionRemindersAuthProvider() -> RemindersAuthorizationProviding {
|
|
return RemindersAuthorizationProvider()
|
|
}
|
|
|
|
func ProductionRemindersListsProvider() -> RemindersListsProviding {
|
|
return EventKitRemindersProvider()
|
|
}
|
|
|
|
func ProductionRemindersListProvider() -> RemindersListProviding {
|
|
return EventKitRemindersProvider()
|
|
}
|
|
|
|
func ProductionRemindersCreateProvider() -> RemindersCreateProviding {
|
|
return EventKitRemindersProvider()
|
|
}
|