diff --git a/Package.swift b/Package.swift index 87d07c65d6a04fa2c353e2e1c914a0bc1db1453c..49a7cd827aee04a45a5e85ba0fe355a52bb1532b 100644 --- a/Package.swift +++ b/Package.swift @@ -466,6 +466,9 @@ let package = Package( .product(name: "ChatLayout", package: "ChatLayout"), .product(name: "DifferenceKit", package: "DifferenceKit"), .product(name: "ScrollViewController", package: "ScrollViewController"), + ], + resources: [ + .process("Resources"), ] ), .testTarget( diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index 3d2a35d19c39a1b06a703a794e089e6adf196769..d2f7f95a8f78c2c7cee0264f9bf43fe7b802bd66 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -430,8 +430,9 @@ public final class SingleChatController: UIViewController { drawer.dismiss(animated: true) { [weak self] in guard let self = self else { return } self.drawerCancellables.removeAll() - self.viewModel.proceeedWithReport(screenshot: self.takeAppScreenshot()) - self.navigationController?.popViewController(animated: true) + self.viewModel.proceeedWithReport(screenshot: self.takeAppScreenshot()) { + self.navigationController?.popViewController(animated: true) + } } }.store(in: &drawerCancellables) @@ -447,18 +448,11 @@ public final class SingleChatController: UIViewController { } func takeAppScreenshot() -> UIImage { - let foregroundWindowScene: UIWindowScene? = UIApplication.shared.connectedScenes - .filter { $0.activationState == .foregroundActive } - .compactMap { $0 as? UIWindowScene } - .first - guard let foregroundWindowScene = foregroundWindowScene else { fatalError("[takeAppScreenshot]: Unable to get foreground window scene") } - guard let keyWindow = foregroundWindowScene.windows.first(where: \.isKeyWindow) else { - fatalError("[takeAppScreenshot]: Unable to get key window") - } + let keyWindow = getKeyWindow(foregroundWindowScene) let rendererFormat = UIGraphicsImageRendererFormat() rendererFormat.scale = foregroundWindowScene.screen.scale @@ -602,11 +596,15 @@ extension SingleChatController: KeyboardListenerDelegate { } func keyboardWillChangeFrame(info: KeyboardInfo) { - let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first + guard let scene = foregroundWindowScene else { + fatalError("[keyboardWillChangeFrame]: Couldn't get foregroundWindowScene") + } + + let keyWindow = getKeyWindow(scene) + let keyboardFrame = keyWindow.convert(info.frameEnd, to: view) guard !currentInterfaceActions.options.contains(.changingFrameSize), collectionView.contentInsetAdjustmentBehavior != .never, - let keyboardFrame = keyWindow?.convert(info.frameEnd, to: view), collectionView.convert(collectionView.bounds, to: keyWindow).maxY > info.frameEnd.minY else { return } currentInterfaceActions.options.insert(.changingKeyboardFrame) @@ -766,3 +764,16 @@ extension SingleChatController: QLPreviewControllerDelegate { fileURL = nil } } + +let foregroundWindowScene: UIWindowScene? = UIApplication.shared.connectedScenes + .filter { $0.activationState == .foregroundActive } + .compactMap { $0 as? UIWindowScene } + .first + +func getKeyWindow(_ scene: UIWindowScene) -> UIWindow { + guard let keyWindow = scene.windows.first(where: \.isKeyWindow) else { + fatalError("Unable to get key window") + } + + return keyWindow +} diff --git a/Sources/ChatFeature/Resources/report_cert.crt b/Sources/ChatFeature/Resources/report_cert.crt new file mode 100644 index 0000000000000000000000000000000000000000..be1d50ad2e61be90d6725bb98292ac46b25b440b --- /dev/null +++ b/Sources/ChatFeature/Resources/report_cert.crt @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +WuHZNZ2W6k8sb2qp0vBbj6TXfhs= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index d4aa517ba54e33073824556891e46c8c7766f606..2a1e34930b4bcd9fc4a577346da743e37a1577b6 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -7,6 +7,7 @@ import XXLogger import XXModels import Foundation import Integration +import Defaults import Permissions import ToastFeature import DifferenceKit @@ -23,12 +24,14 @@ enum SingleChatNavigationRoutes: Equatable { case webview(String) } -final class SingleChatViewModel { +final class SingleChatViewModel: NSObject { @Dependency private var logger: XXLogger @Dependency private var session: SessionType @Dependency private var permissions: PermissionHandling @Dependency private var toastController: ToastController + @KeyObject(.username, defaultValue: nil) var username: String? + var contact: Contact { contactSubject.value } private var stagedReply: Reply? private var cancellables = Set<AnyCancellable>() @@ -73,6 +76,7 @@ final class SingleChatViewModel { init(_ contact: Contact) { self.contactSubject = .init(contact) + super.init() updateRecentState(contact) @@ -140,11 +144,11 @@ final class SingleChatViewModel { guard let id = message.id else { return } session.retryMessage(id) } - + func didNavigateSomewhere() { navigationRoutes.send(.none) } - + @discardableResult func didTest(permission: PermissionType) -> Bool { switch permission { @@ -222,18 +226,6 @@ final class SingleChatViewModel { return (contactTitle, message.text) } - func proceeedWithReport(screenshot: UIImage) { - var contact = contact - contact.isBlocked = true - _ = try? session.dbManager.saveContact(contact) - - let name = (contact.nickname ?? contact.username) ?? "" - toastController.enqueueToast(model: .init( - title: "Your report has been sent and \(name) is now blocked.", - leftImage: Asset.requestSentToaster.image - )) - } - func showRoundFrom(_ roundURL: String?) { if let urlString = roundURL, !urlString.isEmpty { navigationRoutes.send(.webview(urlString)) @@ -261,3 +253,117 @@ final class SingleChatViewModel { sectionsRelay.value.count > 0 ? sectionsRelay.value[index].model : nil } } + +extension SingleChatViewModel { + struct Report: Encodable { + struct ReportUser: Encodable { + var userId: String + var username: String + } + + var sender: ReportUser + var recipient: ReportUser + var type: String + var screenshot: String + } + + private func blockContact() { + var contact = contact + contact.isBlocked = true + _ = try? session.dbManager.saveContact(contact) + } + + private func makeReportRequest(with screenshot: UIImage) -> URLRequest { + let url = URL(string: "https://3.74.237.181:11420/report") + + let report = Report( + sender: .init( + userId: contact.id.base64EncodedString(), + username: contact.username! + ), + recipient: .init( + userId: session.myId.base64EncodedString(), + username: username! + ), type: "dm", + screenshot: screenshot.jpegData(compressionQuality: 0.1)!.base64EncodedString()) + + var request = try! URLRequest(url: url!, method: .post) + request.httpBody = try! JSONEncoder().encode(report) + return request + } + + private func enqueueBlockedToast() { + let name = (contact.nickname ?? contact.username) ?? "" + toastController.enqueueToast(model: .init( + title: "Your report has been sent and \(name) is now blocked.", + leftImage: Asset.requestSentToaster.image + )) + } + + private func uploadReport( + _ request: URLRequest, + completion: @escaping (Result<Void, Error>) -> Void + ) { + URLSession(configuration: .default, delegate: self, delegateQueue: nil) + .dataTask(with: request) { data, response, error in + if let error = error as? NSError { + completion(.failure(error)) + return + } + + if let data = data { + completion(.success(())) + } + }.resume() + } + + func proceeedWithReport(screenshot: UIImage, completion: @escaping () -> Void) { + hudRelay.send(.on) + + uploadReport(makeReportRequest(with: screenshot)) { [weak self] in + guard let self = self else { return } + + switch $0 { + case .success: + DispatchQueue.main.async { + self.blockContact() + self.enqueueBlockedToast() + self.hudRelay.send(.none) + completion() + } + + case .failure(let error): + DispatchQueue.main.async { + self.hudRelay.send(.error(.init(with: error))) + completion() + } + } + } + } +} + +extension SingleChatViewModel: URLSessionDelegate { + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + let serverTrust = challenge.protectionSpace.serverTrust + let certificate = SecTrustGetCertificateAtIndex(serverTrust!, 0) + + let policies = NSMutableArray() + policies.add(SecPolicyCreateSSL(true, challenge.protectionSpace.host as CFString)) + SecTrustSetPolicies(serverTrust!, policies) + + let remoteCertificateData: NSData = SecCertificateCopyData(certificate!) + let pathToCert = Bundle.module.path(forResource: "report_cert", ofType: "crt") + let localCertificate: NSData = NSData(contentsOfFile: pathToCert!)! + + if (remoteCertificateData.isEqual(to: localCertificate as Data)) { + let credential: URLCredential = URLCredential(trust: serverTrust!) + completionHandler(.useCredential, credential) + } else { + completionHandler(.cancelAuthenticationChallenge, nil) + } + } +}