NightscoutUtils.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. //
  2. // NightscoutUtils.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2023-04-09.
  6. // Copyright © 2023 Jon Fawcett. All rights reserved.
  7. //
  8. import Foundation
  9. class NightscoutUtils {
  10. enum NightscoutError {
  11. case emptyAddress
  12. case invalidURL
  13. case networkError
  14. case siteNotFound
  15. case invalidToken
  16. case tokenRequired
  17. case unknown
  18. }
  19. enum EventType: String {
  20. case cage = "Site Change"
  21. case carbsToday = "Carb Correction"
  22. case sage = "Sensor Start"
  23. case sgv
  24. case profile
  25. case treatments
  26. case deviceStatus
  27. var endpoint: String {
  28. switch self {
  29. case .cage, .carbsToday, .sage, .treatments:
  30. return "/api/v1/treatments.json"
  31. case .sgv:
  32. return "/api/v1/entries.json"
  33. case .profile:
  34. return "/api/v1/profile/current.json"
  35. case .deviceStatus:
  36. return "/api/v1/devicestatus.json"
  37. }
  38. }
  39. }
  40. static func executeRequest<T: Decodable>(eventType: EventType, parameters: [String: String], completion: @escaping (Result<T, Error>) -> Void) {
  41. let baseURL = ObservableUserDefaults.shared.url.value
  42. let token = UserDefaultsRepository.token.value
  43. guard let url = NightscoutUtils.constructURL(baseURL: baseURL, token: token, endpoint: eventType.endpoint, parameters: parameters) else {
  44. completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to construct URL"])))
  45. return
  46. }
  47. var request = URLRequest(url: url)
  48. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  49. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  50. guard let data = data, error == nil else {
  51. completion(.failure(error!))
  52. return
  53. }
  54. let decoder = JSONDecoder()
  55. do {
  56. let decodedObject = try decoder.decode(T.self, from: data)
  57. DispatchQueue.main.async {
  58. completion(.success(decodedObject))
  59. }
  60. } catch {
  61. completion(.failure(error))
  62. }
  63. }
  64. task.resume()
  65. }
  66. static func executeDynamicRequest(eventType: EventType, parameters: [String: String], completion: @escaping (Result<Any, Error>) -> Void) {
  67. let baseURL = ObservableUserDefaults.shared.url.value
  68. let token = UserDefaultsRepository.token.value
  69. guard let url = NightscoutUtils.constructURL(baseURL: baseURL, token: token, endpoint: eventType.endpoint, parameters: parameters) else {
  70. completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to construct URL"])))
  71. return
  72. }
  73. var request = URLRequest(url: url)
  74. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  75. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  76. guard let data = data, error == nil else {
  77. completion(.failure(error!))
  78. return
  79. }
  80. do {
  81. if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
  82. DispatchQueue.main.async {
  83. completion(.success(jsonObject))
  84. }
  85. } else if let jsonArray = try JSONSerialization.jsonObject(with: data, options: []) as? [Any] {
  86. DispatchQueue.main.async {
  87. completion(.success(jsonArray))
  88. }
  89. } else {
  90. completion(.failure(NSError(domain: "NightscoutUtils", code: -2, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON Structure"])))
  91. }
  92. } catch {
  93. completion(.failure(error))
  94. }
  95. }
  96. task.resume()
  97. }
  98. static func createURLRequest(url: String, token: String?, path: String) -> URLRequest? {
  99. var requestURLString = "\(url)\(path)"
  100. if let token = token {
  101. let encodedToken = token.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? token
  102. requestURLString += "?token=\(encodedToken)"
  103. }
  104. guard let requestURL = URL(string: requestURLString) else {
  105. return nil
  106. }
  107. var request = URLRequest(url: requestURL)
  108. request.httpMethod = "GET"
  109. return request
  110. }
  111. static func constructURL(baseURL: String, token: String?, endpoint: String, parameters: [String: String]) -> URL? {
  112. var components = URLComponents(string: baseURL)
  113. components?.path = endpoint
  114. var queryItems = [URLQueryItem]()
  115. if let token = token, !token.isEmpty {
  116. queryItems.append(URLQueryItem(name: "token", value: token))
  117. }
  118. for (key, value) in parameters {
  119. queryItems.append(URLQueryItem(name: key, value: value))
  120. }
  121. components?.queryItems = queryItems
  122. return components?.url
  123. }
  124. static func verifyURLAndToken(completion: @escaping (NightscoutError?, String?, Bool) -> Void) {
  125. let urlUser = ObservableUserDefaults.shared.url.value
  126. let token = UserDefaultsRepository.token.value
  127. if urlUser.isEmpty {
  128. completion(.emptyAddress, nil, false)
  129. return
  130. }
  131. guard let _ = URL(string: urlUser), urlUser.hasPrefix("http://") || urlUser.hasPrefix("https://") else {
  132. completion(.invalidURL, nil, false)
  133. return
  134. }
  135. guard let request = createURLRequest(url: urlUser, token: token, path: "/api/v1/status.json") else {
  136. completion(.invalidURL, nil, false)
  137. return
  138. }
  139. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  140. var nsWriteAuth = false
  141. if let httpResponse = response as? HTTPURLResponse {
  142. switch httpResponse.statusCode {
  143. case 200:
  144. if let data = data {
  145. do {
  146. if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
  147. let authorized = jsonResponse["authorized"] as? [String: Any],
  148. let token = authorized["token"] as? String,
  149. let permissionGroups = authorized["permissionGroups"] as? [[String]] {
  150. if permissionGroups.contains(where: { $0.contains("*") }) {
  151. nsWriteAuth = true
  152. } else if permissionGroups.contains(where: { $0.contains("api:treatments:create") }) {
  153. nsWriteAuth = true
  154. }
  155. completion(nil, token, nsWriteAuth)
  156. } else {
  157. completion(.invalidToken, nil, false)
  158. }
  159. } catch {
  160. completion(.invalidToken, nil, false)
  161. }
  162. } else {
  163. completion(.invalidToken, nil, false)
  164. }
  165. case 401:
  166. if token.isEmpty {
  167. completion(.tokenRequired, nil, false)
  168. } else {
  169. completion(.invalidToken, nil, false)
  170. }
  171. default:
  172. completion(.unknown, nil, false)
  173. }
  174. } else {
  175. if let _ = error {
  176. completion(.siteNotFound, nil, false)
  177. } else {
  178. completion(.networkError, nil, false)
  179. }
  180. }
  181. }
  182. task.resume()
  183. }
  184. static func parseDate(_ dateString: String) -> Date? {
  185. let dateFormatterWithMilliseconds = DateFormatter()
  186. dateFormatterWithMilliseconds.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
  187. dateFormatterWithMilliseconds.timeZone = TimeZone(abbreviation: "UTC")
  188. dateFormatterWithMilliseconds.locale = Locale(identifier: "en_US_POSIX")
  189. let dateFormatterWithoutMilliseconds = DateFormatter()
  190. dateFormatterWithoutMilliseconds.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
  191. dateFormatterWithoutMilliseconds.timeZone = TimeZone(abbreviation: "UTC")
  192. dateFormatterWithoutMilliseconds.locale = Locale(identifier: "en_US_POSIX")
  193. if let date = dateFormatterWithMilliseconds.date(from: dateString) {
  194. return date
  195. } else if let date = dateFormatterWithoutMilliseconds.date(from: dateString) {
  196. return date
  197. }
  198. return nil
  199. }
  200. static func retrieveJWTToken(completion: @escaping (NightscoutError?, String?) -> Void) {
  201. let urlUser = ObservableUserDefaults.shared.url.value
  202. let token = UserDefaultsRepository.token.value
  203. if urlUser.isEmpty {
  204. completion(.emptyAddress, nil)
  205. return
  206. }
  207. guard let _ = URL(string: urlUser), urlUser.hasPrefix("http://") || urlUser.hasPrefix("https://") else {
  208. completion(.invalidURL, nil)
  209. return
  210. }
  211. guard let request = createURLRequest(url: urlUser, token: token, path: "/api/v1/status.json") else {
  212. completion(.invalidURL, nil)
  213. return
  214. }
  215. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  216. if let httpResponse = response as? HTTPURLResponse {
  217. switch httpResponse.statusCode {
  218. case 200:
  219. if let data = data {
  220. do {
  221. if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
  222. let authorized = jsonResponse["authorized"] as? [String: Any],
  223. let jwtToken = authorized["token"] as? String {
  224. completion(nil, jwtToken)
  225. } else {
  226. completion(.invalidToken, nil)
  227. }
  228. } catch {
  229. completion(.invalidToken, nil)
  230. }
  231. } else {
  232. completion(.invalidToken, nil)
  233. }
  234. case 401:
  235. if token.isEmpty {
  236. completion(.tokenRequired, nil)
  237. } else {
  238. completion(.invalidToken, nil)
  239. }
  240. default:
  241. completion(.unknown, nil)
  242. }
  243. } else {
  244. if let _ = error {
  245. completion(.siteNotFound, nil)
  246. } else {
  247. completion(.networkError, nil)
  248. }
  249. }
  250. }
  251. task.resume()
  252. }
  253. static func executePostRequest<T: Decodable>(eventType: EventType, body: [String: Any], completion: @escaping (Result<T, Error>) -> Void) {
  254. retrieveJWTToken { error, jwtToken in
  255. if let error = error {
  256. completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "Token retrieval failed: \(error)"])))
  257. return
  258. }
  259. guard let jwtToken = jwtToken else {
  260. completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "No JWT token received"])))
  261. return
  262. }
  263. let baseURL = ObservableUserDefaults.shared.url.value
  264. guard let url = URL(string: "\(baseURL)\(eventType.endpoint)") else {
  265. completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
  266. return
  267. }
  268. var request = URLRequest(url: url)
  269. request.httpMethod = "POST"
  270. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  271. request.setValue("Bearer \(jwtToken)", forHTTPHeaderField: "Authorization")
  272. do {
  273. request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
  274. } catch {
  275. completion(.failure(NSError(domain: "NightscoutUtils", code: -2, userInfo: [NSLocalizedDescriptionKey: "Invalid body parameters"])))
  276. return
  277. }
  278. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  279. guard let data = data, error == nil else {
  280. completion(.failure(error!))
  281. return
  282. }
  283. print("Network response received on thread: \(Thread.current)")
  284. let decoder = JSONDecoder()
  285. do {
  286. let decodedObject = try decoder.decode(T.self, from: data)
  287. completion(.success(decodedObject))
  288. } catch DecodingError.typeMismatch(let type, let context) {
  289. print("Type mismatch error: \(type), \(context.debugDescription)")
  290. do {
  291. let fallbackDecodedObject = try JSONSerialization.jsonObject(with: data, options: [])
  292. print("Fallback JSON decoding success: \(fallbackDecodedObject)")
  293. } catch {
  294. completion(.failure(error))
  295. }
  296. } catch {
  297. completion(.failure(error))
  298. }
  299. }
  300. task.resume()
  301. }
  302. }
  303. }