ApiService.swift 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import Foundation
  2. class ApiService {
  3. static let shared = ApiService()
  4. static let userAgent = "ntfy/\(Config.version) (build \(Config.build); iOS \(Config.osVersion))"
  5. private let tag = "ApiService"
  6. func poll(subscription: Subscription, user: BasicUser?, completionHandler: @escaping ([Message]?, Error?) -> Void) {
  7. guard let url = URL(string: subscription.urlString()) else {
  8. // FIXME
  9. return
  10. }
  11. let since = subscription.lastNotificationId ?? "all"
  12. let urlString = "\(url)/json?poll=1&since=\(since)"
  13. Log.d(tag, "Polling from \(urlString) with user \(user?.username ?? "anonymous")")
  14. fetchJsonData(urlString: urlString, user: user, completionHandler: completionHandler)
  15. }
  16. func poll(subscription: Subscription, messageId: String, user: BasicUser?, completionHandler: @escaping (Message?, Error?) -> Void) {
  17. let url = URL(string: "\(subscription.urlString())/json?poll=1&id=\(messageId)")!
  18. Log.d(tag, "Polling single message from \(url) with user \(user?.username ?? "anonymous")")
  19. let request = newRequest(url: url, user: user)
  20. newSession(timeout: 30).dataTask(with: request) { (data, response, error) in
  21. if let error = error {
  22. completionHandler(nil, error)
  23. return
  24. }
  25. do {
  26. let message = try JSONDecoder().decode(Message.self, from: data!)
  27. completionHandler(message, nil)
  28. } catch {
  29. completionHandler(nil, error)
  30. }
  31. }.resume()
  32. }
  33. func publish(
  34. subscription: Subscription,
  35. user: BasicUser?,
  36. message: String,
  37. title: String,
  38. priority: Int = 3,
  39. tags: [String] = []
  40. ) {
  41. guard let url = URL(string: subscription.urlString()) else { return }
  42. var request = newRequest(url: url, user: user)
  43. Log.d(tag, "Publishing to \(url)")
  44. request.httpMethod = "POST"
  45. request.setValue(title, forHTTPHeaderField: "Title")
  46. request.setValue(String(priority), forHTTPHeaderField: "Priority")
  47. request.setValue(tags.joined(separator: ","), forHTTPHeaderField: "Tags")
  48. request.httpBody = message.data(using: String.Encoding.utf8)
  49. newSession(timeout: 10).dataTask(with: request) { (data, response, error) in
  50. guard error == nil else {
  51. Log.e(self.tag, "Error publishing message", error!)
  52. return
  53. }
  54. Log.d(self.tag, "Publishing message succeeded", response)
  55. }.resume()
  56. }
  57. func checkAuth(baseUrl: String, topic: String, user: BasicUser?, completionHandler: @escaping(AuthResult) -> Void) {
  58. guard let url = URL(string: topicAuthUrl(baseUrl: baseUrl, topic: topic)) else { return }
  59. let request = newRequest(url: url, user: user)
  60. Log.d(tag, "Checking auth for \(url) with user \(user?.username ?? "anonymous")")
  61. newSession(timeout: 10).dataTask(with: request) { (data, response, error) in
  62. if let error = error {
  63. Log.e(self.tag, "Error checking auth: \(error)")
  64. completionHandler(.Error(error.localizedDescription))
  65. } else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
  66. if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {
  67. completionHandler(.Unauthorized)
  68. } else {
  69. completionHandler(.Error("Unexpected response from server: \(httpResponse.statusCode)"))
  70. }
  71. } else if let data = data {
  72. do {
  73. let result = try JSONDecoder().decode(AuthCheckResponse.self, from: data)
  74. Log.d(self.tag, "Auth result: \(result)")
  75. if result.success == true {
  76. completionHandler(.Success)
  77. } else {
  78. completionHandler(.Error("Unexpected response from server"))
  79. }
  80. } catch {
  81. Log.e(self.tag, "Error handling auth response: \(error)")
  82. completionHandler(.Error("Unexpected response from server. Is this a ntfy server?"))
  83. }
  84. }
  85. }.resume()
  86. }
  87. private func fetchJsonData<T: Decodable>(urlString: String, user: BasicUser?, completionHandler: @escaping ([T]?, Error?) -> ()) {
  88. guard let url = URL(string: urlString) else { return }
  89. let request = newRequest(url: url, user: user)
  90. newSession(timeout: 30).dataTask(with: request) { (data, response, error) in
  91. if let error = error {
  92. Log.e(self.tag, "Error fetching data", error)
  93. completionHandler(nil, error)
  94. return
  95. }
  96. do {
  97. let lines = String(decoding: data!, as: UTF8.self).split(whereSeparator: \.isNewline)
  98. var notifications: [T] = []
  99. for jsonLine in lines {
  100. notifications.append(try JSONDecoder().decode(T.self, from: jsonLine.data(using: .utf8)!))
  101. }
  102. completionHandler(notifications, nil)
  103. } catch {
  104. Log.e(self.tag, "Error fetching data", error)
  105. completionHandler(nil, error)
  106. }
  107. }.resume()
  108. }
  109. private func newRequest(url: URL, user: BasicUser?) -> URLRequest {
  110. var request = URLRequest(url: url)
  111. request.setValue(ApiService.userAgent, forHTTPHeaderField: "User-Agent")
  112. if let user = user {
  113. request.setValue(user.toHeader(), forHTTPHeaderField: "Authorization")
  114. }
  115. return request
  116. }
  117. private func newSession(timeout: TimeInterval) -> URLSession {
  118. let sessionConfig = URLSessionConfiguration.default
  119. sessionConfig.timeoutIntervalForRequest = timeout
  120. sessionConfig.timeoutIntervalForResource = timeout
  121. return URLSession(configuration: sessionConfig)
  122. }
  123. }
  124. struct BasicUser {
  125. let username: String
  126. let password: String
  127. func toHeader() -> String {
  128. return "Basic " + String(format: "%@:%@", username, password).data(using: String.Encoding.utf8)!.base64EncodedString()
  129. }
  130. }
  131. enum AuthResult {
  132. case Success
  133. case Unauthorized
  134. case Error(String)
  135. }
  136. struct AuthCheckResponse: Codable {
  137. let success: Bool?
  138. let code: Int?
  139. let http: Int?
  140. let error: String?
  141. enum CodingKeys: String, CodingKey {
  142. case success, code, http, error
  143. }
  144. init(from decoder: Decoder) throws {
  145. let container = try decoder.container(keyedBy: CodingKeys.self)
  146. self.success = try container.decodeIfPresent(Bool.self, forKey: .success)
  147. self.code = try container.decodeIfPresent(Int.self, forKey: .code)
  148. self.http = try container.decodeIfPresent(Int.self, forKey: .http)
  149. self.error = try container.decodeIfPresent(String.self, forKey: .error)
  150. }
  151. }