120 lines
4.2 KiB
Swift
120 lines
4.2 KiB
Swift
import Foundation
|
|
import UserNotifications
|
|
|
|
enum PingService {
|
|
private static let stateStore = PingStateStore()
|
|
|
|
static func suppressChecks(for hostname: String, duration: TimeInterval) async {
|
|
await stateStore.suppressChecks(for: hostname, duration: duration)
|
|
}
|
|
|
|
static func ping(hostname: String, apiKey: String, notificationsEnabled: Bool = true) async -> Bool {
|
|
if await stateStore.shouldSkipPing(for: hostname) {
|
|
return false
|
|
}
|
|
|
|
guard let url = URL(string: "https://\(hostname)/api/v2/ping") else {
|
|
print("❌ [PingService] Invalid URL for \(hostname)")
|
|
return false
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY")
|
|
request.timeoutInterval = 10
|
|
|
|
do {
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
|
await handlePingFailure(for: hostname, notificationsEnabled: notificationsEnabled)
|
|
return false
|
|
}
|
|
|
|
if let result = try? JSONDecoder().decode([String: String].self, from: data), result["response"] == "pong" {
|
|
await handlePingSuccess(for: hostname, notificationsEnabled: notificationsEnabled)
|
|
return true
|
|
} else {
|
|
await handlePingFailure(for: hostname, notificationsEnabled: notificationsEnabled)
|
|
return false
|
|
}
|
|
} catch {
|
|
await handlePingFailure(for: hostname, notificationsEnabled: notificationsEnabled)
|
|
return false
|
|
}
|
|
}
|
|
|
|
private static func handlePingSuccess(for hostname: String, notificationsEnabled: Bool) async {
|
|
if let notification = await stateStore.recordSuccess(for: hostname, notificationsEnabled: notificationsEnabled) {
|
|
sendNotification(title: notification.title, body: notification.body)
|
|
}
|
|
}
|
|
|
|
private static func handlePingFailure(for hostname: String, notificationsEnabled: Bool) async {
|
|
if let notification = await stateStore.recordFailure(for: hostname, notificationsEnabled: notificationsEnabled) {
|
|
sendNotification(title: notification.title, body: notification.body)
|
|
}
|
|
}
|
|
|
|
private static func sendNotification(title: String, body: String) {
|
|
let content = UNMutableNotificationContent()
|
|
content.title = title
|
|
content.body = body
|
|
content.sound = .default
|
|
|
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
|
UNUserNotificationCenter.current().add(request)
|
|
}
|
|
}
|
|
|
|
private actor PingStateStore {
|
|
private var previousPingStates: [String: Bool] = [:]
|
|
private var suppressedUntil: [String: Date] = [:]
|
|
|
|
func suppressChecks(for hostname: String, duration: TimeInterval) {
|
|
suppressedUntil[hostname] = Date().addingTimeInterval(duration)
|
|
previousPingStates[hostname] = false
|
|
}
|
|
|
|
func shouldSkipPing(for hostname: String) -> Bool {
|
|
if let suppressedUntil = suppressedUntil[hostname], suppressedUntil > Date() {
|
|
return true
|
|
}
|
|
|
|
suppressedUntil.removeValue(forKey: hostname)
|
|
return false
|
|
}
|
|
|
|
func recordSuccess(for hostname: String, notificationsEnabled: Bool) -> PingNotification? {
|
|
let wasPreviouslyDown = previousPingStates[hostname] == false
|
|
previousPingStates[hostname] = true
|
|
|
|
guard wasPreviouslyDown, notificationsEnabled else {
|
|
return nil
|
|
}
|
|
|
|
return PingNotification(
|
|
title: "Server Online",
|
|
body: "\(hostname) is now online"
|
|
)
|
|
}
|
|
|
|
func recordFailure(for hostname: String, notificationsEnabled: Bool) -> PingNotification? {
|
|
let wasPreviouslyUp = previousPingStates[hostname] != false
|
|
previousPingStates[hostname] = false
|
|
|
|
guard wasPreviouslyUp, notificationsEnabled else {
|
|
return nil
|
|
}
|
|
|
|
return PingNotification(
|
|
title: "Server Offline",
|
|
body: "\(hostname) is offline"
|
|
)
|
|
}
|
|
}
|
|
|
|
private struct PingNotification {
|
|
let title: String
|
|
let body: String
|
|
}
|