From edcc30808cad67780dc6bc5191bcc1e501e2c8d8 Mon Sep 17 00:00:00 2001 From: dab246 Date: Sun, 24 Dec 2023 19:31:03 +0700 Subject: [PATCH] TF-2384 Create TwakeCore in ios folder to handle network jmap request to server Signed-off-by: dab246 (cherry picked from commit d8a330b987860f133b7e56b4aa67669c9d3c8f20) --- .../ShareViewController.swift | 8 +- ios/TwakeCore/Exceptions/JmapExceptions.swift | 15 +++ .../Exceptions/NetworkExceptions.swift | 11 +++ ios/TwakeCore/Jmap/JmapClient.swift | 51 ++++++++++ ios/TwakeCore/Jmap/Model/Email/Email.swift | 8 ++ .../Jmap/Model/Email/EmailAddress.swift | 6 ++ .../Model/Request/JmapRequestObject.swift | 68 ++++++++++++++ .../Model/Response/JmapResponseObject.swift | 92 +++++++++++++++++++ ios/TwakeCore/Jmap/Utils/JmapConstants.swift | 18 ++++ .../Jmap/Utils/JmapRequestGenerator.swift | 44 +++++++++ ios/TwakeCore/Logger/TwakeLogger.swift | 9 ++ ios/TwakeCore/Network/AlamofireService.swift | 30 ++++++ .../Network/Model/AuthenticationType.swift | 5 + ios/TwakeCore/Network/Model/TokenOidc.swift | 14 +++ 14 files changed, 375 insertions(+), 4 deletions(-) create mode 100644 ios/TwakeCore/Exceptions/JmapExceptions.swift create mode 100644 ios/TwakeCore/Exceptions/NetworkExceptions.swift create mode 100644 ios/TwakeCore/Jmap/JmapClient.swift create mode 100644 ios/TwakeCore/Jmap/Model/Email/Email.swift create mode 100644 ios/TwakeCore/Jmap/Model/Email/EmailAddress.swift create mode 100644 ios/TwakeCore/Jmap/Model/Request/JmapRequestObject.swift create mode 100644 ios/TwakeCore/Jmap/Model/Response/JmapResponseObject.swift create mode 100644 ios/TwakeCore/Jmap/Utils/JmapConstants.swift create mode 100644 ios/TwakeCore/Jmap/Utils/JmapRequestGenerator.swift create mode 100644 ios/TwakeCore/Logger/TwakeLogger.swift create mode 100644 ios/TwakeCore/Network/AlamofireService.swift create mode 100644 ios/TwakeCore/Network/Model/AuthenticationType.swift create mode 100644 ios/TwakeCore/Network/Model/TokenOidc.swift diff --git a/ios/TeamMailShareExtension/ShareViewController.swift b/ios/TeamMailShareExtension/ShareViewController.swift index 056b68707..5ece90a08 100644 --- a/ios/TeamMailShareExtension/ShareViewController.swift +++ b/ios/TeamMailShareExtension/ShareViewController.swift @@ -63,7 +63,7 @@ class ShareViewController: SLComposeServiceViewController { } override func didSelectPost() { - print("didSelectPost"); + TwakeLogger.shared.log(message: "didSelectPost"); } override func configurationItems() -> [Any]! { @@ -204,7 +204,7 @@ class ShareViewController: SLComposeServiceViewController { private func dismissWithError() { - print("[ERROR] Error loading data!") + TwakeLogger.shared.log(message: "[ERROR] Error loading data!") let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert) let action = UIAlertAction(title: "Error", style: .cancel) { _ in @@ -275,7 +275,7 @@ class ShareViewController: SLComposeServiceViewController { } try FileManager.default.copyItem(at: srcURL, to: dstURL) } catch (let error) { - print("Cannot copy item at \(srcURL) to \(dstURL): \(error)") + TwakeLogger.shared.log(message: "Cannot copy item at \(srcURL) to \(dstURL): \(error)") return false } return true @@ -331,7 +331,7 @@ class ShareViewController: SLComposeServiceViewController { // Debug method to print out SharedMediaFile details in the console func toString() { - print("[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(self.thumbnail)\n\tduration: \(self.duration)\n\ttype: \(self.type)") + TwakeLogger.shared.log(message: "[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(self.thumbnail)\n\tduration: \(self.duration)\n\ttype: \(self.type)") } } diff --git a/ios/TwakeCore/Exceptions/JmapExceptions.swift b/ios/TwakeCore/Exceptions/JmapExceptions.swift new file mode 100644 index 000000000..460f99b8c --- /dev/null +++ b/ios/TwakeCore/Exceptions/JmapExceptions.swift @@ -0,0 +1,15 @@ +import Foundation + +class JmapExceptions: Error, Equatable { + static func == (value1: JmapExceptions, value2: JmapExceptions) -> Bool { + return value1.value == value2.value + } + + static let notFoundNewEmails = JmapExceptions(value: "Not found news emails") + + var value: String? + + init(value: String? = nil) { + self.value = value + } +} diff --git a/ios/TwakeCore/Exceptions/NetworkExceptions.swift b/ios/TwakeCore/Exceptions/NetworkExceptions.swift new file mode 100644 index 000000000..fb8bb4185 --- /dev/null +++ b/ios/TwakeCore/Exceptions/NetworkExceptions.swift @@ -0,0 +1,11 @@ +import Foundation + +class NetworkExceptions: Error { + static let requestUrlInvalid = NetworkExceptions(value: "Request url invalid") + + var value: String? + + init(value: String? = nil) { + self.value = value + } +} diff --git a/ios/TwakeCore/Jmap/JmapClient.swift b/ios/TwakeCore/Jmap/JmapClient.swift new file mode 100644 index 000000000..ac9c83e34 --- /dev/null +++ b/ios/TwakeCore/Jmap/JmapClient.swift @@ -0,0 +1,51 @@ +import Foundation +import Alamofire + +class JmapClient { + static let shared: JmapClient = JmapClient() + + private let jmapHeader = HTTPHeader(name: "Accept", value: "application/json;jmapVersion=rfc-8621") + + private func getBasicAuthorization(basicAuth: String) -> String { + return "Basic \(basicAuth)" + } + + func getNewEmails( + apiUrl: String, + accountId: String, + sinceState: String, + authenticationType: AuthenticationType, + tokenOidc: TokenOidc?, + basicAuth: String?, + onSuccess: @escaping ([Email]) -> Void, + onFailure: @escaping (Error) -> Void + ) { + let authenticationValue = authenticationType == AuthenticationType.basic + ? getBasicAuthorization(basicAuth: basicAuth ?? "") + : tokenOidc?.getAuthorization() ?? "" + + let headers = HTTPHeaders([ + jmapHeader, + HTTPHeader(name: "Authorization", value: authenticationValue) + ]) + + let jmapRequestObject = JmapRequestGenerator.shared.createEmailChangesRequest( + accountId: accountId, + sinceState: sinceState + ) + + AlamofireService.shared.post( + url: apiUrl, + payloadData: jmapRequestObject?.toData(), + headers: headers, + onSuccess: { (data: JmapResponseObject) in + if let listEmail = data.parsing(methodName: JmapConstants.EMAIL_GET_METHOD_NAME, methodCallId: "c1"), !listEmail.isEmpty { + onSuccess(listEmail) + } else { + onFailure(JmapExceptions.notFoundNewEmails) + } + }, + onFailure: onFailure + ) + } +} diff --git a/ios/TwakeCore/Jmap/Model/Email/Email.swift b/ios/TwakeCore/Jmap/Model/Email/Email.swift new file mode 100644 index 000000000..70a09e658 --- /dev/null +++ b/ios/TwakeCore/Jmap/Model/Email/Email.swift @@ -0,0 +1,8 @@ +import Foundation + +struct Email: Codable { + let id: String + let subject: String? + let preview: String? + let from: [EmailAddress]? +} diff --git a/ios/TwakeCore/Jmap/Model/Email/EmailAddress.swift b/ios/TwakeCore/Jmap/Model/Email/EmailAddress.swift new file mode 100644 index 000000000..4bb7fd97b --- /dev/null +++ b/ios/TwakeCore/Jmap/Model/Email/EmailAddress.swift @@ -0,0 +1,6 @@ +import Foundation + +struct EmailAddress: Codable { + let name: String? + let email: String? +} diff --git a/ios/TwakeCore/Jmap/Model/Request/JmapRequestObject.swift b/ios/TwakeCore/Jmap/Model/Request/JmapRequestObject.swift new file mode 100644 index 000000000..83d46fa12 --- /dev/null +++ b/ios/TwakeCore/Jmap/Model/Request/JmapRequestObject.swift @@ -0,0 +1,68 @@ +import Foundation + +struct JmapRequestObject: Codable { + let using: [String] + let methodCalls: [[RequestInvocation]] +} + +enum RequestInvocation: Codable { + case methodRequest(MethodRequest) + case string(String) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let x = try? container.decode(String.self) { + self = .string(x) + return + } + if let x = try? container.decode(MethodRequest.self) { + self = .methodRequest(x) + return + } + throw DecodingError.typeMismatch(RequestInvocation.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for RequestInvocation")) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .methodRequest(let x): + try container.encode(x) + case .string(let x): + try container.encode(x) + } + } +} + +struct MethodRequest: Codable { + let accountId: String + let sinceState: String? + let ids: ResultReference? + let properties: [String]? + + enum CodingKeys: String, CodingKey { + case accountId + case sinceState + case ids = "#ids" + case properties + } +} + +struct ResultReference: Codable { + let resultOf, name, path: String +} + +extension JmapRequestObject { + func toData() -> Data? { + if let encodedData = try? JSONEncoder().encode(self) { + return encodedData + } + return nil + } + + func toJson() -> String? { + if let data = toData(), let jsonString = String(data: data, encoding: .utf8) { + return jsonString + } + return nil + } +} diff --git a/ios/TwakeCore/Jmap/Model/Response/JmapResponseObject.swift b/ios/TwakeCore/Jmap/Model/Response/JmapResponseObject.swift new file mode 100644 index 000000000..e6fb2d743 --- /dev/null +++ b/ios/TwakeCore/Jmap/Model/Response/JmapResponseObject.swift @@ -0,0 +1,92 @@ +import Foundation + +struct JmapResponseObject: Codable { + let sessionState: String + let methodResponses: [[ResponseInvocation]] +} + +enum ResponseInvocation: Codable { + case methodResponse(MethodResponse) + case string(String) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let x = try? container.decode(String.self) { + self = .string(x) + return + } + if let x = try? container.decode(MethodResponse.self) { + self = .methodResponse(x) + return + } + throw DecodingError.typeMismatch(ResponseInvocation.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for ResponseInvocation")) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .methodResponse(let x): + try container.encode(x) + case .string(let x): + try container.encode(x) + } + } + + var listObject: [T]? { + switch self { + case .methodResponse(let method): return method.list + case .string(_): return nil + } + } +} + +struct MethodResponse: Codable { + let accountId: String + let oldState: String? + let newState: String? + let hasMoreChanges: Bool? + let created: [String]? + let updated: [String]? + let destroyed: [String]? + let notFound: [String]? + let state: String? + let list: [T]? +} + +extension JmapResponseObject { + func parsing(methodName: String, methodCallId: String) -> [T]? { + let invocations = methodResponses.first { (responseInvocations: [ResponseInvocation]) in + return responseInvocations.contains { (responseInvocation: ResponseInvocation) in + return validateResponseInvocation(response: responseInvocation, methodName: methodName, methodCallId: methodCallId) + } + } + + if invocations == nil || invocations?.isEmpty == true { + return nil + } else { + let invocation = invocations!.first { response in + return validateMethodResponse(response: response) + } + + return invocation?.listObject + } + } + + private func validateResponseInvocation(response: ResponseInvocation, methodName: String, methodCallId: String) -> Bool { + switch response { + case .string(let stringValue): + return stringValue == methodName || stringValue == methodCallId + default: + return false + } + } + + private func validateMethodResponse(response: ResponseInvocation) -> Bool { + switch response { + case .methodResponse(_): + return true + default: + return false + } + } +} diff --git a/ios/TwakeCore/Jmap/Utils/JmapConstants.swift b/ios/TwakeCore/Jmap/Utils/JmapConstants.swift new file mode 100644 index 000000000..11b6e4600 --- /dev/null +++ b/ios/TwakeCore/Jmap/Utils/JmapConstants.swift @@ -0,0 +1,18 @@ +import Foundation + +class JmapConstants { + static let EMAIL_GET_METHOD_NAME = "Email/get" + static let EMAIL_CHANGES_METHOD_NAME = "Email/changes" + + static let CREATED_REFERENCE_PATH = "/created/*" + + static let JMAP_CORE_CAPABILITY = "urn:ietf:params:jmap:core" + static let JMAP_MAIL_CAPABILITY = "urn:ietf:params:jmap:mail" + + static let EMAIL_PUSH_NOTIFICATION_PROPERTIES = [ + "id", + "subject", + "preview", + "from" + ] +} diff --git a/ios/TwakeCore/Jmap/Utils/JmapRequestGenerator.swift b/ios/TwakeCore/Jmap/Utils/JmapRequestGenerator.swift new file mode 100644 index 000000000..e0da6446d --- /dev/null +++ b/ios/TwakeCore/Jmap/Utils/JmapRequestGenerator.swift @@ -0,0 +1,44 @@ +import Foundation + +class JmapRequestGenerator { + static let shared: JmapRequestGenerator = JmapRequestGenerator() + + func createEmailChangesRequest(accountId: String, sinceState: String) -> JmapRequestObject? { + return JmapRequestObject( + using: [ + JmapConstants.JMAP_CORE_CAPABILITY, + JmapConstants.JMAP_MAIL_CAPABILITY + ], + methodCalls: [ + [ + RequestInvocation.string(JmapConstants.EMAIL_CHANGES_METHOD_NAME), + RequestInvocation.methodRequest( + MethodRequest( + accountId: accountId, + sinceState: sinceState, + ids: nil, + properties: nil + ) + ), + RequestInvocation.string("c0"), + ], + [ + RequestInvocation.string(JmapConstants.EMAIL_GET_METHOD_NAME), + RequestInvocation.methodRequest( + MethodRequest( + accountId: accountId, + sinceState: nil, + ids: ResultReference( + resultOf: "c0", + name: JmapConstants.EMAIL_CHANGES_METHOD_NAME, + path: JmapConstants.CREATED_REFERENCE_PATH + ), + properties: JmapConstants.EMAIL_PUSH_NOTIFICATION_PROPERTIES + ) + ), + RequestInvocation.string("c1"), + ] + ] + ) + } +} diff --git a/ios/TwakeCore/Logger/TwakeLogger.swift b/ios/TwakeCore/Logger/TwakeLogger.swift new file mode 100644 index 000000000..4a5a16dcd --- /dev/null +++ b/ios/TwakeCore/Logger/TwakeLogger.swift @@ -0,0 +1,9 @@ +import Foundation + +class TwakeLogger { + static let shared: TwakeLogger = TwakeLogger() + + func log(message: String) { + debugPrint(message) + } +} diff --git a/ios/TwakeCore/Network/AlamofireService.swift b/ios/TwakeCore/Network/AlamofireService.swift new file mode 100644 index 000000000..892d0f06d --- /dev/null +++ b/ios/TwakeCore/Network/AlamofireService.swift @@ -0,0 +1,30 @@ +import Foundation +import Alamofire + +class AlamofireService { + static let shared: AlamofireService = AlamofireService() + + func post( + url: String, + payloadData: Data?, + headers: HTTPHeaders? = nil, + onSuccess: @escaping (T) -> Void, + onFailure: @escaping (Error) -> Void + ) { + guard let baseUrl = URL(string: url), + var request = try? URLRequest(url: baseUrl, method: .post, headers: headers) else { + return onFailure(NetworkExceptions.requestUrlInvalid) + } + + request.httpBody = payloadData + + AF.request(request).responseDecodable(of: T.self) { response in + switch(response.result) { + case .success(let data): + onSuccess(data) + case .failure(let error): + onFailure(NetworkExceptions(value: "\(error.localizedDescription)")) + } + } + } +} diff --git a/ios/TwakeCore/Network/Model/AuthenticationType.swift b/ios/TwakeCore/Network/Model/AuthenticationType.swift new file mode 100644 index 000000000..0e8dc07f4 --- /dev/null +++ b/ios/TwakeCore/Network/Model/AuthenticationType.swift @@ -0,0 +1,5 @@ +import Foundation + +enum AuthenticationType: String, Codable { + case basic, oidc, none +} diff --git a/ios/TwakeCore/Network/Model/TokenOidc.swift b/ios/TwakeCore/Network/Model/TokenOidc.swift new file mode 100644 index 000000000..8aebc23c2 --- /dev/null +++ b/ios/TwakeCore/Network/Model/TokenOidc.swift @@ -0,0 +1,14 @@ +import Foundation + +struct TokenOidc: Codable { + let token: String + let tokenId: String + let expiredTime: String? + let refreshToken: String +} + +extension TokenOidc { + func getAuthorization() -> String { + return "Bearer \(token)" + } +}