| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- //
- // NightscoutUtils.swift
- // LoopFollow
- //
- // Created by Jonas Björkert on 2023-04-09.
- // Copyright © 2023 Jon Fawcett. All rights reserved.
- //
- import Foundation
- class NightscoutUtils {
- enum NightscoutError {
- case emptyAddress
- case invalidURL
- case networkError
- case siteNotFound
- case invalidToken
- case tokenRequired
- case unknown
- }
-
- enum EventType: String {
- case cage = "Site Change"
- case carbsToday = "Carb Correction"
- case sage = "Sensor Start"
- case sgv
- case profile
- case treatments
- case deviceStatus
-
- var endpoint: String {
- switch self {
- case .cage, .carbsToday, .sage, .treatments:
- return "/api/v1/treatments.json"
- case .sgv:
- return "/api/v1/entries.json"
- case .profile:
- return "/api/v1/profile/current.json"
- case .deviceStatus:
- return "/api/v1/devicestatus.json"
- }
- }
- }
-
- static func executeRequest<T: Decodable>(eventType: EventType, parameters: [String: String], completion: @escaping (Result<T, Error>) -> Void) {
- let baseURL = ObservableUserDefaults.shared.url.value
- let token = UserDefaultsRepository.token.value
-
- guard let url = NightscoutUtils.constructURL(baseURL: baseURL, token: token, endpoint: eventType.endpoint, parameters: parameters) else {
- completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to construct URL"])))
- return
- }
-
- var request = URLRequest(url: url)
- request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
-
- let task = URLSession.shared.dataTask(with: request) { data, response, error in
- guard let data = data, error == nil else {
- completion(.failure(error!))
- return
- }
-
- let decoder = JSONDecoder()
- do {
- let decodedObject = try decoder.decode(T.self, from: data)
- DispatchQueue.main.async {
- completion(.success(decodedObject))
- }
- } catch {
- completion(.failure(error))
- }
- }
- task.resume()
- }
-
-
- static func executeDynamicRequest(eventType: EventType, parameters: [String: String], completion: @escaping (Result<Any, Error>) -> Void) {
- let baseURL = ObservableUserDefaults.shared.url.value
- let token = UserDefaultsRepository.token.value
-
- guard let url = NightscoutUtils.constructURL(baseURL: baseURL, token: token, endpoint: eventType.endpoint, parameters: parameters) else {
- completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to construct URL"])))
- return
- }
-
- var request = URLRequest(url: url)
- request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
-
- let task = URLSession.shared.dataTask(with: request) { data, response, error in
- guard let data = data, error == nil else {
- completion(.failure(error!))
- return
- }
-
- do {
- if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
- DispatchQueue.main.async {
- completion(.success(jsonObject))
- }
- } else if let jsonArray = try JSONSerialization.jsonObject(with: data, options: []) as? [Any] {
- DispatchQueue.main.async {
- completion(.success(jsonArray))
- }
- } else {
- completion(.failure(NSError(domain: "NightscoutUtils", code: -2, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON Structure"])))
- }
- } catch {
- completion(.failure(error))
- }
- }
- task.resume()
- }
-
- static func createURLRequest(url: String, token: String?, path: String) -> URLRequest? {
- var requestURLString = "\(url)\(path)"
-
- if let token = token {
- let encodedToken = token.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? token
- requestURLString += "?token=\(encodedToken)"
- }
-
- guard let requestURL = URL(string: requestURLString) else {
- return nil
- }
-
- var request = URLRequest(url: requestURL)
- request.httpMethod = "GET"
- return request
- }
-
- static func constructURL(baseURL: String, token: String?, endpoint: String, parameters: [String: String]) -> URL? {
- var components = URLComponents(string: baseURL)
- components?.path = endpoint
-
- var queryItems = [URLQueryItem]()
-
- if let token = token, !token.isEmpty {
- queryItems.append(URLQueryItem(name: "token", value: token))
- }
-
- for (key, value) in parameters {
- queryItems.append(URLQueryItem(name: key, value: value))
- }
-
- components?.queryItems = queryItems
-
- return components?.url
- }
-
- static func verifyURLAndToken(completion: @escaping (NightscoutError?, String?, Bool) -> Void) {
- let urlUser = ObservableUserDefaults.shared.url.value
- let token = UserDefaultsRepository.token.value
-
- if urlUser.isEmpty {
- completion(.emptyAddress, nil, false)
- return
- }
-
- guard let _ = URL(string: urlUser), urlUser.hasPrefix("http://") || urlUser.hasPrefix("https://") else {
- completion(.invalidURL, nil, false)
- return
- }
-
- guard let request = createURLRequest(url: urlUser, token: token, path: "/api/v1/status.json") else {
- completion(.invalidURL, nil, false)
- return
- }
-
- let task = URLSession.shared.dataTask(with: request) { data, response, error in
- var nsWriteAuth = false
-
- if let httpResponse = response as? HTTPURLResponse {
- switch httpResponse.statusCode {
- case 200:
- if let data = data {
- do {
- if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
- let authorized = jsonResponse["authorized"] as? [String: Any],
- let token = authorized["token"] as? String,
- let permissionGroups = authorized["permissionGroups"] as? [[String]] {
-
- if permissionGroups.contains(where: { $0.contains("*") }) {
- nsWriteAuth = true
- } else if permissionGroups.contains(where: { $0.contains("api:treatments:create") }) {
- nsWriteAuth = true
- }
- completion(nil, token, nsWriteAuth)
- } else {
- completion(.invalidToken, nil, false)
- }
- } catch {
- completion(.invalidToken, nil, false)
- }
- } else {
- completion(.invalidToken, nil, false)
- }
- case 401:
- if token.isEmpty {
- completion(.tokenRequired, nil, false)
- } else {
- completion(.invalidToken, nil, false)
- }
- default:
- completion(.unknown, nil, false)
- }
- } else {
- if let _ = error {
- completion(.siteNotFound, nil, false)
- } else {
- completion(.networkError, nil, false)
- }
- }
- }
- task.resume()
- }
-
- static func parseDate(_ dateString: String) -> Date? {
- let dateFormatterWithMilliseconds = DateFormatter()
- dateFormatterWithMilliseconds.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
- dateFormatterWithMilliseconds.timeZone = TimeZone(abbreviation: "UTC")
- dateFormatterWithMilliseconds.locale = Locale(identifier: "en_US_POSIX")
-
- let dateFormatterWithoutMilliseconds = DateFormatter()
- dateFormatterWithoutMilliseconds.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
- dateFormatterWithoutMilliseconds.timeZone = TimeZone(abbreviation: "UTC")
- dateFormatterWithoutMilliseconds.locale = Locale(identifier: "en_US_POSIX")
-
- if let date = dateFormatterWithMilliseconds.date(from: dateString) {
- return date
- } else if let date = dateFormatterWithoutMilliseconds.date(from: dateString) {
- return date
- }
-
- return nil
- }
-
- static func retrieveJWTToken(completion: @escaping (NightscoutError?, String?) -> Void) {
- let urlUser = ObservableUserDefaults.shared.url.value
- let token = UserDefaultsRepository.token.value
-
- if urlUser.isEmpty {
- completion(.emptyAddress, nil)
- return
- }
-
- guard let _ = URL(string: urlUser), urlUser.hasPrefix("http://") || urlUser.hasPrefix("https://") else {
- completion(.invalidURL, nil)
- return
- }
-
- guard let request = createURLRequest(url: urlUser, token: token, path: "/api/v1/status.json") else {
- completion(.invalidURL, nil)
- return
- }
-
- let task = URLSession.shared.dataTask(with: request) { data, response, error in
- if let httpResponse = response as? HTTPURLResponse {
- switch httpResponse.statusCode {
- case 200:
- if let data = data {
- do {
- if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
- let authorized = jsonResponse["authorized"] as? [String: Any],
- let jwtToken = authorized["token"] as? String {
- completion(nil, jwtToken)
- } else {
- completion(.invalidToken, nil)
- }
- } catch {
- completion(.invalidToken, nil)
- }
- } else {
- completion(.invalidToken, nil)
- }
- case 401:
- if token.isEmpty {
- completion(.tokenRequired, nil)
- } else {
- completion(.invalidToken, nil)
- }
- default:
- completion(.unknown, nil)
- }
- } else {
- if let _ = error {
- completion(.siteNotFound, nil)
- } else {
- completion(.networkError, nil)
- }
- }
-
- }
- task.resume()
- }
-
- static func executePostRequest<T: Decodable>(eventType: EventType, body: [String: Any], completion: @escaping (Result<T, Error>) -> Void) {
- retrieveJWTToken { error, jwtToken in
- if let error = error {
- completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "Token retrieval failed: \(error)"])))
- return
- }
-
- guard let jwtToken = jwtToken else {
- completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "No JWT token received"])))
- return
- }
-
- let baseURL = ObservableUserDefaults.shared.url.value
-
- guard let url = URL(string: "\(baseURL)\(eventType.endpoint)") else {
- completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
- return
- }
-
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- request.setValue("Bearer \(jwtToken)", forHTTPHeaderField: "Authorization")
-
- do {
- request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
- } catch {
- completion(.failure(NSError(domain: "NightscoutUtils", code: -2, userInfo: [NSLocalizedDescriptionKey: "Invalid body parameters"])))
- return
- }
-
- let task = URLSession.shared.dataTask(with: request) { data, response, error in
- guard let data = data, error == nil else {
- completion(.failure(error!))
- return
- }
- print("Network response received on thread: \(Thread.current)")
- let decoder = JSONDecoder()
- do {
- let decodedObject = try decoder.decode(T.self, from: data)
- completion(.success(decodedObject))
- } catch DecodingError.typeMismatch(let type, let context) {
- print("Type mismatch error: \(type), \(context.debugDescription)")
- do {
- let fallbackDecodedObject = try JSONSerialization.jsonObject(with: data, options: [])
- print("Fallback JSON decoding success: \(fallbackDecodedObject)")
- } catch {
- completion(.failure(error))
- }
- } catch {
- completion(.failure(error))
- }
- }
- task.resume()
- }
- }
- }
|