123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- import Foundation
- class ApiService {
- static let shared = ApiService()
- static let userAgent = "ntfy/\(Config.version) (build \(Config.build); iOS \(Config.osVersion))"
-
- private let tag = "ApiService"
-
- func poll(subscription: Subscription, user: BasicUser?, completionHandler: @escaping ([Message]?, Error?) -> Void) {
- guard let url = URL(string: subscription.urlString()) else {
- // FIXME
- return
- }
- let since = subscription.lastNotificationId ?? "all"
- let urlString = "\(url)/json?poll=1&since=\(since)"
-
- Log.d(tag, "Polling from \(urlString) with user \(user?.username ?? "anonymous")")
- fetchJsonData(urlString: urlString, user: user, completionHandler: completionHandler)
- }
-
- func poll(subscription: Subscription, messageId: String, user: BasicUser?, completionHandler: @escaping (Message?, Error?) -> Void) {
- let url = URL(string: "\(subscription.urlString())/json?poll=1&id=\(messageId)")!
- Log.d(tag, "Polling single message from \(url) with user \(user?.username ?? "anonymous")")
-
- let request = newRequest(url: url, user: user)
- newSession(timeout: 30).dataTask(with: request) { (data, response, error) in
- if let error = error {
- completionHandler(nil, error)
- return
- }
- do {
- let message = try JSONDecoder().decode(Message.self, from: data!)
- completionHandler(message, nil)
- } catch {
- completionHandler(nil, error)
- }
- }.resume()
- }
- func publish(
- subscription: Subscription,
- user: BasicUser?,
- message: String,
- title: String,
- priority: Int = 3,
- tags: [String] = []
- ) {
- guard let url = URL(string: subscription.urlString()) else { return }
- var request = newRequest(url: url, user: user)
- Log.d(tag, "Publishing to \(url)")
-
- request.httpMethod = "POST"
- request.setValue(title, forHTTPHeaderField: "Title")
- request.setValue(String(priority), forHTTPHeaderField: "Priority")
- request.setValue(tags.joined(separator: ","), forHTTPHeaderField: "Tags")
- request.httpBody = message.data(using: String.Encoding.utf8)
- newSession(timeout: 10).dataTask(with: request) { (data, response, error) in
- guard error == nil else {
- Log.e(self.tag, "Error publishing message", error!)
- return
- }
- Log.d(self.tag, "Publishing message succeeded", response)
- }.resume()
- }
-
- func checkAuth(baseUrl: String, topic: String, user: BasicUser?, completionHandler: @escaping(AuthResult) -> Void) {
- guard let url = URL(string: topicAuthUrl(baseUrl: baseUrl, topic: topic)) else { return }
- let request = newRequest(url: url, user: user)
- Log.d(tag, "Checking auth for \(url) with user \(user?.username ?? "anonymous")")
- newSession(timeout: 10).dataTask(with: request) { (data, response, error) in
- if let error = error {
- Log.e(self.tag, "Error checking auth: \(error)")
- completionHandler(.Error(error.localizedDescription))
- } else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
- if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {
- completionHandler(.Unauthorized)
- } else {
- completionHandler(.Error("Unexpected response from server: \(httpResponse.statusCode)"))
- }
- } else if let data = data {
- do {
- let result = try JSONDecoder().decode(AuthCheckResponse.self, from: data)
- Log.d(self.tag, "Auth result: \(result)")
- if result.success == true {
- completionHandler(.Success)
- } else {
- completionHandler(.Error("Unexpected response from server"))
- }
- } catch {
- Log.e(self.tag, "Error handling auth response: \(error)")
- completionHandler(.Error("Unexpected response from server. Is this a ntfy server?"))
- }
- }
- }.resume()
- }
- private func fetchJsonData<T: Decodable>(urlString: String, user: BasicUser?, completionHandler: @escaping ([T]?, Error?) -> ()) {
- guard let url = URL(string: urlString) else { return }
- let request = newRequest(url: url, user: user)
- newSession(timeout: 30).dataTask(with: request) { (data, response, error) in
- if let error = error {
- Log.e(self.tag, "Error fetching data", error)
- completionHandler(nil, error)
- return
- }
- do {
- let lines = String(decoding: data!, as: UTF8.self).split(whereSeparator: \.isNewline)
- var notifications: [T] = []
- for jsonLine in lines {
- notifications.append(try JSONDecoder().decode(T.self, from: jsonLine.data(using: .utf8)!))
- }
- completionHandler(notifications, nil)
- } catch {
- Log.e(self.tag, "Error fetching data", error)
- completionHandler(nil, error)
- }
- }.resume()
- }
-
- private func newRequest(url: URL, user: BasicUser?) -> URLRequest {
- var request = URLRequest(url: url)
- request.setValue(ApiService.userAgent, forHTTPHeaderField: "User-Agent")
- if let user = user {
- request.setValue(user.toHeader(), forHTTPHeaderField: "Authorization")
- }
- return request
- }
-
- private func newSession(timeout: TimeInterval) -> URLSession {
- let sessionConfig = URLSessionConfiguration.default
- sessionConfig.timeoutIntervalForRequest = timeout
- sessionConfig.timeoutIntervalForResource = timeout
- return URLSession(configuration: sessionConfig)
- }
- }
- struct BasicUser {
- let username: String
- let password: String
-
- func toHeader() -> String {
- return "Basic " + String(format: "%@:%@", username, password).data(using: String.Encoding.utf8)!.base64EncodedString()
- }
- }
- enum AuthResult {
- case Success
- case Unauthorized
- case Error(String)
- }
- struct AuthCheckResponse: Codable {
- let success: Bool?
- let code: Int?
- let http: Int?
- let error: String?
- enum CodingKeys: String, CodingKey {
- case success, code, http, error
- }
- init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
- self.success = try container.decodeIfPresent(Bool.self, forKey: .success)
- self.code = try container.decodeIfPresent(Int.self, forKey: .code)
- self.http = try container.decodeIfPresent(Int.self, forKey: .http)
- self.error = try container.decodeIfPresent(String.self, forKey: .error)
- }
- }
|