You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							291 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							291 lines
						
					
					
						
							11 KiB
						
					
					
				
								//
							 | 
						|
								//  DailyNotificationCallbacks.swift
							 | 
						|
								//  DailyNotificationPlugin
							 | 
						|
								//
							 | 
						|
								//  Created by Matthew Raymer on 2025-09-22
							 | 
						|
								//  Copyright © 2025 TimeSafari. All rights reserved.
							 | 
						|
								//
							 | 
						|
								
							 | 
						|
								import Foundation
							 | 
						|
								import CoreData
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Callback management for iOS Daily Notification Plugin
							 | 
						|
								 * Implements HTTP and local callback delivery with error handling
							 | 
						|
								 * 
							 | 
						|
								 * @author Matthew Raymer
							 | 
						|
								 * @version 1.1.0
							 | 
						|
								 * @created 2025-09-22 09:22:32 UTC
							 | 
						|
								 */
							 | 
						|
								extension DailyNotificationPlugin {
							 | 
						|
								    
							 | 
						|
								    // MARK: - Callback Management
							 | 
						|
								    
							 | 
						|
								    @objc func registerCallback(_ call: CAPPluginCall) {
							 | 
						|
								        guard let name = call.getString("name"),
							 | 
						|
								              let callbackConfig = call.getObject("callback") else {
							 | 
						|
								            call.reject("Callback name and config required")
							 | 
						|
								            return
							 | 
						|
								        }
							 | 
						|
								        
							 | 
						|
								        print("DNP-PLUGIN: Registering callback: \(name)")
							 | 
						|
								        
							 | 
						|
								        do {
							 | 
						|
								            try registerCallback(name: name, config: callbackConfig)
							 | 
						|
								            call.resolve()
							 | 
						|
								        } catch {
							 | 
						|
								            print("DNP-PLUGIN: Failed to register callback: \(error)")
							 | 
						|
								            call.reject("Callback registration failed: \(error.localizedDescription)")
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    @objc func unregisterCallback(_ call: CAPPluginCall) {
							 | 
						|
								        guard let name = call.getString("name") else {
							 | 
						|
								            call.reject("Callback name required")
							 | 
						|
								            return
							 | 
						|
								        }
							 | 
						|
								        
							 | 
						|
								        print("DNP-PLUGIN: Unregistering callback: \(name)")
							 | 
						|
								        
							 | 
						|
								        do {
							 | 
						|
								            try unregisterCallback(name: name)
							 | 
						|
								            call.resolve()
							 | 
						|
								        } catch {
							 | 
						|
								            print("DNP-PLUGIN: Failed to unregister callback: \(error)")
							 | 
						|
								            call.reject("Callback unregistration failed: \(error.localizedDescription)")
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    @objc func getRegisteredCallbacks(_ call: CAPPluginCall) {
							 | 
						|
								        Task {
							 | 
						|
								            do {
							 | 
						|
								                let callbacks = try await getRegisteredCallbacks()
							 | 
						|
								                call.resolve(["callbacks": callbacks])
							 | 
						|
								            } catch {
							 | 
						|
								                print("DNP-PLUGIN: Failed to get registered callbacks: \(error)")
							 | 
						|
								                call.reject("Callback retrieval failed: \(error.localizedDescription)")
							 | 
						|
								            }
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    // MARK: - Content Management
							 | 
						|
								    
							 | 
						|
								    @objc func getContentCache(_ call: CAPPluginCall) {
							 | 
						|
								        Task {
							 | 
						|
								            do {
							 | 
						|
								                let cache = try await getContentCache()
							 | 
						|
								                call.resolve(cache)
							 | 
						|
								            } catch {
							 | 
						|
								                print("DNP-PLUGIN: Failed to get content cache: \(error)")
							 | 
						|
								                call.reject("Content cache retrieval failed: \(error.localizedDescription)")
							 | 
						|
								            }
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    @objc func clearContentCache(_ call: CAPPluginCall) {
							 | 
						|
								        Task {
							 | 
						|
								            do {
							 | 
						|
								                try await clearContentCache()
							 | 
						|
								                call.resolve()
							 | 
						|
								            } catch {
							 | 
						|
								                print("DNP-PLUGIN: Failed to clear content cache: \(error)")
							 | 
						|
								                call.reject("Content cache clearing failed: \(error.localizedDescription)")
							 | 
						|
								            }
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    @objc func getContentHistory(_ call: CAPPluginCall) {
							 | 
						|
								        Task {
							 | 
						|
								            do {
							 | 
						|
								                let history = try await getContentHistory()
							 | 
						|
								                call.resolve(["history": history])
							 | 
						|
								            } catch {
							 | 
						|
								                print("DNP-PLUGIN: Failed to get content history: \(error)")
							 | 
						|
								                call.reject("Content history retrieval failed: \(error.localizedDescription)")
							 | 
						|
								            }
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    // MARK: - Private Callback Implementation
							 | 
						|
								    
							 | 
						|
								    private func fireCallbacks(eventType: String, payload: [String: Any]) async throws {
							 | 
						|
								        // Get registered callbacks from Core Data
							 | 
						|
								        let context = persistenceController.container.viewContext
							 | 
						|
								        let request: NSFetchRequest<Callback> = Callback.fetchRequest()
							 | 
						|
								        request.predicate = NSPredicate(format: "enabled == YES")
							 | 
						|
								        
							 | 
						|
								        let callbacks = try context.fetch(request)
							 | 
						|
								        
							 | 
						|
								        for callback in callbacks {
							 | 
						|
								            do {
							 | 
						|
								                try await deliverCallback(callback: callback, eventType: eventType, payload: payload)
							 | 
						|
								            } catch {
							 | 
						|
								                print("DNP-CB-FAILURE: Callback \(callback.id ?? "unknown") failed: \(error)")
							 | 
						|
								            }
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    private func deliverCallback(callback: Callback, eventType: String, payload: [String: Any]) async throws {
							 | 
						|
								        guard let callbackId = callback.id,
							 | 
						|
								              let kind = callback.kind else { return }
							 | 
						|
								        
							 | 
						|
								        let event = [
							 | 
						|
								            "id": callbackId,
							 | 
						|
								            "at": Date().timeIntervalSince1970,
							 | 
						|
								            "type": eventType,
							 | 
						|
								            "payload": payload
							 | 
						|
								        ] as [String: Any]
							 | 
						|
								        
							 | 
						|
								        switch kind {
							 | 
						|
								        case "http":
							 | 
						|
								            try await deliverHttpCallback(callback: callback, event: event)
							 | 
						|
								        case "local":
							 | 
						|
								            try await deliverLocalCallback(callback: callback, event: event)
							 | 
						|
								        default:
							 | 
						|
								            print("DNP-CB-UNKNOWN: Unknown callback kind: \(kind)")
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    private func deliverHttpCallback(callback: Callback, event: [String: Any]) async throws {
							 | 
						|
								        guard let target = callback.target,
							 | 
						|
								              let url = URL(string: target) else {
							 | 
						|
								            throw NSError(domain: "DailyNotificationPlugin", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid callback target"])
							 | 
						|
								        }
							 | 
						|
								        
							 | 
						|
								        var request = URLRequest(url: url)
							 | 
						|
								        request.httpMethod = "POST"
							 | 
						|
								        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
							 | 
						|
								        request.httpBody = try JSONSerialization.data(withJSONObject: event)
							 | 
						|
								        
							 | 
						|
								        let (_, response) = try await URLSession.shared.data(for: request)
							 | 
						|
								        
							 | 
						|
								        guard let httpResponse = response as? HTTPURLResponse,
							 | 
						|
								              httpResponse.statusCode == 200 else {
							 | 
						|
								            throw NSError(domain: "DailyNotificationPlugin", code: 2, userInfo: [NSLocalizedDescriptionKey: "HTTP callback failed"])
							 | 
						|
								        }
							 | 
						|
								        
							 | 
						|
								        print("DNP-CB-HTTP-SUCCESS: HTTP callback delivered to \(target)")
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    private func deliverLocalCallback(callback: Callback, event: [String: Any]) async throws {
							 | 
						|
								        // Local callback implementation would go here
							 | 
						|
								        print("DNP-CB-LOCAL: Local callback delivered for \(callback.id ?? "unknown")")
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    private func registerCallback(name: String, config: [String: Any]) throws {
							 | 
						|
								        let context = persistenceController.container.viewContext
							 | 
						|
								        
							 | 
						|
								        let callback = Callback(context: context)
							 | 
						|
								        callback.id = name
							 | 
						|
								        callback.kind = config["kind"] as? String ?? "local"
							 | 
						|
								        callback.target = config["target"] as? String ?? ""
							 | 
						|
								        callback.enabled = true
							 | 
						|
								        callback.createdAt = Date()
							 | 
						|
								        
							 | 
						|
								        try context.save()
							 | 
						|
								        print("DNP-CB-REGISTER: Callback \(name) registered")
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    private func unregisterCallback(name: String) throws {
							 | 
						|
								        let context = persistenceController.container.viewContext
							 | 
						|
								        let request: NSFetchRequest<Callback> = Callback.fetchRequest()
							 | 
						|
								        request.predicate = NSPredicate(format: "id == %@", name)
							 | 
						|
								        
							 | 
						|
								        let callbacks = try context.fetch(request)
							 | 
						|
								        for callback in callbacks {
							 | 
						|
								            context.delete(callback)
							 | 
						|
								        }
							 | 
						|
								        
							 | 
						|
								        try context.save()
							 | 
						|
								        print("DNP-CB-UNREGISTER: Callback \(name) unregistered")
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    private func getRegisteredCallbacks() async throws -> [String] {
							 | 
						|
								        let context = persistenceController.container.viewContext
							 | 
						|
								        let request: NSFetchRequest<Callback> = Callback.fetchRequest()
							 | 
						|
								        
							 | 
						|
								        let callbacks = try context.fetch(request)
							 | 
						|
								        return callbacks.compactMap { $0.id }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    private func getContentCache() async throws -> [String: Any] {
							 | 
						|
								        guard let latestContent = try await getLatestContent() else {
							 | 
						|
								            return [:]
							 | 
						|
								        }
							 | 
						|
								        return latestContent
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    private func clearContentCache() async throws {
							 | 
						|
								        let context = persistenceController.container.viewContext
							 | 
						|
								        let request: NSFetchRequest<ContentCache> = ContentCache.fetchRequest()
							 | 
						|
								        
							 | 
						|
								        let results = try context.fetch(request)
							 | 
						|
								        for content in results {
							 | 
						|
								            context.delete(content)
							 | 
						|
								        }
							 | 
						|
								        
							 | 
						|
								        try context.save()
							 | 
						|
								        print("DNP-CACHE-CLEAR: Content cache cleared")
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    private func getContentHistory() async throws -> [[String: Any]] {
							 | 
						|
								        let context = persistenceController.container.viewContext
							 | 
						|
								        let request: NSFetchRequest<History> = History.fetchRequest()
							 | 
						|
								        request.sortDescriptors = [NSSortDescriptor(keyPath: \History.occurredAt, ascending: false)]
							 | 
						|
								        request.fetchLimit = 100
							 | 
						|
								        
							 | 
						|
								        let results = try context.fetch(request)
							 | 
						|
								        return results.map { history in
							 | 
						|
								            [
							 | 
						|
								                "id": history.id ?? "",
							 | 
						|
								                "kind": history.kind ?? "",
							 | 
						|
								                "occurredAt": history.occurredAt?.timeIntervalSince1970 ?? 0,
							 | 
						|
								                "outcome": history.outcome ?? "",
							 | 
						|
								                "durationMs": history.durationMs
							 | 
						|
								            ]
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    private func getHealthStatus() async throws -> [String: Any] {
							 | 
						|
								        let context = persistenceController.container.viewContext
							 | 
						|
								        
							 | 
						|
								        // Get next runs (simplified)
							 | 
						|
								        let nextRuns = [Date().addingTimeInterval(3600).timeIntervalSince1970,
							 | 
						|
								                       Date().addingTimeInterval(86400).timeIntervalSince1970]
							 | 
						|
								        
							 | 
						|
								        // Get recent history
							 | 
						|
								        let historyRequest: NSFetchRequest<History> = History.fetchRequest()
							 | 
						|
								        historyRequest.predicate = NSPredicate(format: "occurredAt >= %@", Date().addingTimeInterval(-86400) as NSDate)
							 | 
						|
								        historyRequest.sortDescriptors = [NSSortDescriptor(keyPath: \History.occurredAt, ascending: false)]
							 | 
						|
								        historyRequest.fetchLimit = 10
							 | 
						|
								        
							 | 
						|
								        let recentHistory = try context.fetch(historyRequest)
							 | 
						|
								        let lastOutcomes = recentHistory.map { $0.outcome ?? "" }
							 | 
						|
								        
							 | 
						|
								        // Get cache age
							 | 
						|
								        let cacheRequest: NSFetchRequest<ContentCache> = ContentCache.fetchRequest()
							 | 
						|
								        cacheRequest.sortDescriptors = [NSSortDescriptor(keyPath: \ContentCache.fetchedAt, ascending: false)]
							 | 
						|
								        cacheRequest.fetchLimit = 1
							 | 
						|
								        
							 | 
						|
								        let latestCache = try context.fetch(cacheRequest).first
							 | 
						|
								        let cacheAgeMs = latestCache?.fetchedAt?.timeIntervalSinceNow ?? 0
							 | 
						|
								        
							 | 
						|
								        return [
							 | 
						|
								            "nextRuns": nextRuns,
							 | 
						|
								            "lastOutcomes": lastOutcomes,
							 | 
						|
								            "cacheAgeMs": abs(cacheAgeMs * 1000),
							 | 
						|
								            "staleArmed": abs(cacheAgeMs) > 3600,
							 | 
						|
								            "queueDepth": recentHistory.count,
							 | 
						|
								            "circuitBreakers": [
							 | 
						|
								                "total": 0,
							 | 
						|
								                "open": 0,
							 | 
						|
								                "failures": 0
							 | 
						|
								            ],
							 | 
						|
								            "performance": [
							 | 
						|
								                "avgFetchTime": 0,
							 | 
						|
								                "avgNotifyTime": 0,
							 | 
						|
								                "successRate": 1.0
							 | 
						|
								            ]
							 | 
						|
								        ]
							 | 
						|
								    }
							 | 
						|
								}
							 | 
						|
								
							 |