// /* * Copyright (c) 2021 BWI GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import Foundation import UIKit import MatrixSDK import SwiftUI fileprivate let BWI_ServerDowntimes = "BWI_ServerDowntimes" fileprivate let BWI_ValidAppVersions = "BWI_ValidAppVersions" fileprivate let exampleFile = "exampleDowntime" fileprivate let maintenanceURL = "/_matrix/cmaintenance" @objcMembers class ServerDowntimeDefaultService : NSObject { static let shared = ServerDowntimeDefaultService() var isCurrentDowntime: ServerDowntimeStatus = .none static var manuallyIgnored: Bool = false /// Find the next relevant downtime /// - Returns: ServerDowntime or nil, if there is no downtime in the future or the warning date is still in the future func nextDowntime() -> ServerDowntime? { guard let encodedDowntimes = UserDefaults.standard.object(forKey: BWI_ServerDowntimes) as? [Data] else { return nil } // needs to split up because compiler gets confused var serverDowntimes = encodedDowntimes .compactMap { try? JSONDecoder().decode(ServerDowntime.self, from: $0) } serverDowntimes = serverDowntimes .filter { // remove downtimes with invalid values return ($0.type.elementsEqual(ServerDowntimeType.adhocMessage.rawValue) || $0.type.elementsEqual(ServerDowntimeType.maintenance.rawValue)) && $0.startTime.iso8601Date != nil && $0.endTime.iso8601Date != nil && $0.warningStartTime.iso8601Date != nil } .filter { // remove downtimes that are already done guard let downtimeEndDate = $0.endTime.iso8601Date else { return false } return downtimeEndDate.timeIntervalSinceNow >= 0 } .filter { // remove downtimes where the warning time is still in the future guard let warningStartDate = $0.warningStartTime.iso8601Date else { return false } return warningStartDate.timeIntervalSinceNow <= 0 } // filter Maintenance with wrong date order serverDowntimes = serverDowntimes .filter { guard let startDate = $0.startTime.iso8601Date, let endDate = $0.endTime.iso8601Date, let warningDate = $0.warningStartTime.iso8601Date else { return false } return (warningDate <= startDate) && (startDate <= endDate) } // filter empty messages on ADMHOCMESSAGE type serverDowntimes = serverDowntimes .filter { if $0.type.elementsEqual(ServerDowntimeType.adhocMessage.rawValue) { if let description = $0.description, description.count > 0 { return true } else { return false } } return true } serverDowntimes = serverDowntimes .sorted { // sort downtimes be startTime guard let downtimeStartDate1 = $0.startTime.iso8601Date, let downtimeStartDate2 = $1.startTime.iso8601Date else { return false } return downtimeStartDate1 < downtimeStartDate2 } return serverDowntimes.first } /// Check the state of the next downtime /// - Returns: ongoing, warning or none func nextDowntimeStatus() -> ServerDowntimeStatus { guard let downTime = self.nextDowntime() else { return .none } if let startDate = downTime.startTime.iso8601Date { if startDate.timeIntervalSinceNow < 0 { return .ongoing } } return .warning } private func loadTestMaintanceJSON(forName name: String) -> Data? { if let path = Bundle.main.path(forResource: name, ofType: "json") { do { let jsonString = try String(contentsOfFile: path) return jsonString.data(using: .utf8) } catch { print(error.localizedDescription) } } return nil } // MARK: - Handle Server Response func copyFetchResultToUserDefaults(fetchResult: [String : Any]) throws { if let downtimes = fetchResult["downtime"] as? [[String : Any]] { try copyServerDowntimesToUserDefaults(downtimes: downtimes) } if let versions = fetchResult["versions"] as? [String : Any] { try copyValidAppVersionsToUserDefaults(versions: versions) } } private func copyServerDowntimesToUserDefaults(downtimes: [[String : Any]]) throws { var encodedDowntimes: [Data] = [] for downtimeDict in downtimes { if let serverDowntime = try? ServerDowntime(dict: downtimeDict) { let data = try JSONEncoder().encode(serverDowntime) encodedDowntimes.append(data) } } UserDefaults.standard.set(encodedDowntimes, forKey: BWI_ServerDowntimes) } private func copyValidAppVersionsToUserDefaults(versions: [String : Any]) throws { if let iosVersionsDict = versions["ios"] as? [String : Any] { if let validAppVersions = try? ValidAppVersions(dict: iosVersionsDict) { let data = try JSONEncoder().encode(validAppVersions) UserDefaults.standard.set(data, forKey: BWI_ValidAppVersions) } } } // MARK: - Mockup Fetch func fetchDowntimes(completion: @escaping () -> Void) { if let exampleFetch = loadTestMaintanceJSON(forName: exampleFile) { do { if let fetchResult = try JSONSerialization.jsonObject(with: exampleFetch, options: []) as? [String: Any] { try copyFetchResultToUserDefaults(fetchResult: fetchResult) } } catch { print(error.localizedDescription) } } completion() } } // MARK: - extension ServerDowntimeDefaultService : ServerDowntimeService { func alert(alertType: ServerMaintenanceAlertType, completion: @escaping () -> Void) -> Alert { switch alertType { case .showInvalidAppVersionAlert: return Alert( title: Text(BWIL10n.bwiOutdatedVersionWarningTitle), message: Text(BWIL10n.bwiOutdatedVersionWarningMessage(AppInfo.current.displayName)), dismissButton: .destructive(Text(BWIL10n.bwiOutdatedVersionAppstoreButton), action: { let iTunesLink = BWIBuildSettings.shared.itunesAppLink UIApplication.shared.vc_open(URL(string: iTunesLink)!, completionHandler: nil) })) case .showDowntimeTimeAlert: if BWIBuildSettings.shared.ignoreBlockingMaintenance && isBlocking() { return Alert( title: Text(""), message: Text(downtimeText()), primaryButton: .cancel(Text(BWIL10n.blockingDowntimeAlertIgnoreButton)) { UserDefaults.standard.set(false, forKey: "ServerDownTimeBlockingKey") self.setManuallyIgnored() completion() }, secondaryButton: .destructive(Text(BWIL10n.blockingDowntimeAlertDismissButton)) ) } else { return Alert( title: Text(BWIL10n.downtimeTitle), message: Text(downtimeText() != "" ? BWIL10n.downtimeDefaultMessage + "\n\n" + downtimeText() : BWIL10n.downtimeDefaultMessage), dismissButton: .destructive(Text(isBlocking() ? BWIL10n.blockingDowntimeAlertDismissButton : BWIL10n.downtimeAlertDismissButton)) { if self.isBlocking() { return } else { completion() } }) } case .showServerMaintenanceInfoMessageAlert: if BWIBuildSettings.shared.ignoreBlockingMaintenance && isBlocking() { return Alert( title: Text(""), message: Text(downtimeText()), primaryButton: .cancel(Text(BWIL10n.blockingDowntimeAlertIgnoreButton)) { completion() }, secondaryButton: .destructive(Text(BWIL10n.blockingDowntimeAlertDismissButton)) ) } else { return Alert( title: Text(""), message: Text(downtimeText()), dismissButton: .destructive(Text(isBlocking() ? BWIL10n.blockingDowntimeAlertDismissButton : BWIL10n.downtimeAlertDismissButton)) { if self.isBlocking() { return } else { completion() } }) } case .showServerMaintenanceDefaultAlert: return Alert( title: Text(BWIL10n.downtimeTitle), message: Text(BWIL10n.downtimeDefaultMessage), dismissButton: .destructive(Text(BWIL10n.downtimeAlertDismissButton)) { completion() }) } } func showAlert() -> Bool { return nextDowntimeStatus() == .ongoing } func alertType() -> ServerMaintenanceAlertType { if downtimeType() == .adhocMessage { return .showServerMaintenanceInfoMessageAlert } else { return .showDowntimeTimeAlert } } func fetchDowntimes(session: MXSession, completion: @escaping () -> Void) { session.matrixRestClient.getDowntime(completion: { (jsonResponse, error) in do { if let fetchResult = jsonResponse as? [String : Any] { try self.copyFetchResultToUserDefaults(fetchResult: fetchResult) } } catch { print(error.localizedDescription) } completion() }) } func loadDowntimes(downtimes: [[String : Any]]) throws { try copyServerDowntimesToUserDefaults(downtimes: downtimes) } func fetchDowntimesWithDirectRequest(localUrlString: String? = nil, completion: @escaping (Bool,HTTPURLResponse?,Data?,Error?) -> Void) { var urlString = AppConfigService.shared.serverUrl() if let localUrlString = localUrlString { urlString = localUrlString } guard let url = URL(string: urlString + maintenanceURL) else { completion(false, nil, nil, nil) return } let config = URLSessionConfiguration.default config.httpAdditionalHeaders = ["User-Agent": UserAgentService().bwiUserAgent] config.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData let session : URLSession = URLSession(configuration: config) let task = session.dataTask(with: url) { data, response, error in guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { completion(false, nil, data, error) return } do { if let data = data, let fetchResult = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { try self.copyFetchResultToUserDefaults(fetchResult: fetchResult) } } catch { print(error.localizedDescription) } completion(true, response, data, error) } task.resume() } // MARK: - @objc func isDowntimePresentable() -> Bool { return nextDowntimeStatus() != .none } @objc func downtimeText() -> String { guard let downTime = self.nextDowntime() else { return "" } switch downtimeType() { case .adhocMessage: guard let description = downTime.description else { return "" } return description case .maintenance: guard let startDate = downTime.startTime.iso8601Date, let endDate = downTime.endTime.iso8601Date else { return "" } let dayOfWeekFormatter = DateFormatter() dayOfWeekFormatter.dateFormat = "EEEE" dayOfWeekFormatter.timeZone = TimeZone.current dayOfWeekFormatter.calendar = Calendar.current let dateFormatter = DateFormatter() dateFormatter.dateFormat = "dd.MM.yyyy" dateFormatter.timeZone = TimeZone.current dateFormatter.calendar = Calendar.current let dateTimeFormatter = DateFormatter() dateTimeFormatter.dateFormat = "dd.MM.yyyy HH:mm" dateTimeFormatter.timeZone = TimeZone.current dateTimeFormatter.calendar = Calendar.current let timeFormatter = DateFormatter() timeFormatter.dateFormat = "HH:mm" timeFormatter.timeZone = TimeZone.current timeFormatter.calendar = Calendar.current let utcFormatter = DateFormatter() utcFormatter.dateFormat = "ZZZZZ" utcFormatter.timeZone = TimeZone.current utcFormatter.calendar = Calendar.current if self.isSameDay() { return BWIL10n.settingsDowntimeMessageSameDay(AppInfo.current.displayName, dayOfWeekFormatter.string(from: startDate), dateFormatter.string(from: startDate), timeFormatter.string(from: startDate), timeFormatter.string(from: endDate), utcFormatter.string(from: startDate)).appending(BWIBuildSettings.shared.showMaintenanceInfoMessageType ? "\n\n\(downTime.description ?? "")" : "") } else { return BWIL10n.settingsDowntimeMessageDifferentDays(AppInfo.current.displayName, dayOfWeekFormatter.string(from: startDate), dateTimeFormatter.string(from: startDate), utcFormatter.string(from: startDate), dayOfWeekFormatter.string(from: endDate), dateTimeFormatter.string(from: endDate), utcFormatter.string(from: endDate)).appending(BWIBuildSettings.shared.showMaintenanceInfoMessageType ? "\n\n\(downTime.description ?? "")" : "") } } } @objc func isDowntimeNow() -> Bool { guard let downTime = self.nextDowntime(), let startDate = downTime.startTime.iso8601Date, let endDate = downTime.endTime.iso8601Date else { return false } return startDate <= Date() && Date() <= endDate } @objc func downtimeColor() -> UIColor { switch nextDowntimeStatus() { case .warning: return .yellow case .ongoing: if isBlocking() { return .red } else { return .yellow } default: return .clear } } @objc func downtimeTextColor() -> UIColor { switch nextDowntimeStatus() { case .warning: return .black case .ongoing: if isBlocking() { return .white } else { return .black } default: return .clear } } func downtimeType() -> ServerDowntimeType { guard let downTime = self.nextDowntime() else { return ServerDowntimeType.maintenance } if BWIBuildSettings.shared.showMaintenanceInfoMessageType && downTime.type.elementsEqual(ServerDowntimeType.adhocMessage.rawValue) { return ServerDowntimeType.adhocMessage } else { return ServerDowntimeType.maintenance } } @objc func isSameDay() -> Bool { guard let downTime = self.nextDowntime(), let startDate = downTime.startTime.iso8601Date, let endDate = downTime.endTime.iso8601Date else { return false } return Calendar.current.dateComponents([.day], from: startDate, to: endDate).day == 0 } @objc func isBlocking() -> Bool { guard let downTime = self.nextDowntime() else { return false } return downTime.blocking ?? false } func setManuallyIgnored() { guard let downTime = self.nextDowntime(), let blocking = downTime.blocking else { return } if blocking && BWIBuildSettings.shared.ignoreBlockingMaintenance { ServerDowntimeDefaultService.manuallyIgnored = true } } func isManuallyIgnored() -> Bool { return ServerDowntimeDefaultService.manuallyIgnored } }