sdk/communication/AzureCommunicationChat/Source/Signaling/CommunicationSignalingClient.swift (226 lines of code) (raw):

// -------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // The MIT License (MIT) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the ""Software""), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // // -------------------------------------------------------------------------- import AzureCommunicationCommon import AzureCore import Foundation import Trouter /// Signaling errors enum for errors that might occur when realtime-notifications are started. public enum CommunicationSignalingError: Error { case failedToRefreshToken(String) } /// Handler for signaling errors. public typealias CommunicationSignalingErrorHandler = (CommunicationSignalingError) -> Void /// TrouterTokenRefreshHandler for fetching tokens. internal typealias TrouterTokenRefreshHandler = (_ stopSignalingClient: Bool, Error?) -> Void class CommunicationSignalingClient { private var selfHostedTrouterClient: SelfHostedTrouterClient? private var communicationSkypeTokenProvider: CommunicationSkypeTokenProvider private var trouterUrlRegistrar: TrouterUrlRegistrar? private var logger: ClientLogger private var communicationHandlers: [ChatEventId: CommunicationHandler] = [:] private var configuration: RealTimeNotificationConfiguration? private var configApiVersion: String // Step 1: Initialize with basic properties init( communicationSkypeTokenProvider: CommunicationSkypeTokenProvider, logger: ClientLogger = ClientLoggers.default(tag: "AzureCommunicationSignalingClient") ) throws { self.logger = logger self.communicationSkypeTokenProvider = communicationSkypeTokenProvider self.configApiVersion = "2024-09-01" } // Step 2: Configure with fetched TrouterSettings public func configure(token: String, endpoint: String, completionHandler: @escaping (Result<Void, AzureError>) -> Void) { self.getTrouterSettings(token: token, endpoint: endpoint) { result in switch result { case .success(let configuration): self.configuration = configuration let trouterSkypeTokenHeaderProvider = TrouterSkypetokenAuthHeaderProvider( skypetokenProvider: self.communicationSkypeTokenProvider ) // Remove the "https://" prefix from the URLs var trouterHostname = configuration.trouterServiceUrl.replacingOccurrences(of: "https://", with: "") let registrarBasePath = configuration.registrarServiceUrl.replacingOccurrences(of: "https://", with: "") // Add the suffix "/v3/a" to trouterHostname trouterHostname += "/v4/a" self.selfHostedTrouterClient = SelfHostedTrouterClient.create( withClientVersion: defaultClientVersion, authHeadersProvider: trouterSkypeTokenHeaderProvider, dataCache: nil, trouterHostname: trouterHostname ) guard let regData = defaultRegistrationData else { completionHandler(.failure(AzureError.client("Failed to create TrouterUrlRegistrationData."))) return } guard let trouterUrlRegistrar = TrouterUrlRegistrar.create( with: self.communicationSkypeTokenProvider, registrationData: regData, registrarHostnameAndBasePath: registrarBasePath, maxRegistrationTtlS: 3600 ) as? TrouterUrlRegistrar else { completionHandler(.failure(AzureError.client("Failed to create TrouterUrlRegistrar."))) return } self.trouterUrlRegistrar = trouterUrlRegistrar completionHandler(.success(())) case .failure(let error): completionHandler(.failure(error)) } } } func start() { // Guard to check if selfHostedTrouterClient and trouterUrlRegistrar are initialized guard let selfHostedTrouterClient = self.selfHostedTrouterClient, let trouterUrlRegistrar = self.trouterUrlRegistrar else { logger.error("Failed to start: SelfHostedTrouterClient or TrouterUrlRegistrar is not initialized.") return } selfHostedTrouterClient.withRegistrar(trouterUrlRegistrar) selfHostedTrouterClient.start() selfHostedTrouterClient.setUserActivityState(UserActivityState.TrouterUserActivityStateActive) } func stop() { // Guard to check if selfHostedTrouterClient is initialized guard let selfHostedTrouterClient = self.selfHostedTrouterClient else { logger.error("Failed to stop: SelfHostedTrouterClient is not initialized.") return } selfHostedTrouterClient.stop() communicationHandlers.forEach { _, handler in selfHostedTrouterClient.unregisterListener(handler) } communicationHandlers.removeAll() } func on(event: ChatEventId, handler: @escaping TrouterEventHandler) { // Guard to check if selfHostedTrouterClient is initialized guard let selfHostedTrouterClient = self.selfHostedTrouterClient else { logger.error("Failed to register event handler: SelfHostedTrouterClient is not initialized.") return } let logger = ClientLoggers.default(tag: "AzureCommunicationHandler-\(event)") let communicationHandler = CommunicationHandler(handler: handler, logger: logger) selfHostedTrouterClient.register(communicationHandler, forPath: "/\(event)") communicationHandlers[event] = communicationHandler } func off(event: ChatEventId) { // Guard to check if selfHostedTrouterClient is initialized guard let selfHostedTrouterClient = self.selfHostedTrouterClient else { logger.error("Failed to unregister event handler: SelfHostedTrouterClient is not initialized.") return } if let communicationHandler = communicationHandlers[event] { selfHostedTrouterClient.unregisterListener(communicationHandler) communicationHandlers.removeValue(forKey: event) } } private func getTrouterSettings( token: String, endpoint: String, completionHandler: @escaping (Result<RealTimeNotificationConfiguration, AzureError>) -> Void ) { let urlString = "\(endpoint)/chat/config/realTimeNotifications?api-version=\(configApiVersion)" guard let url = URL(string: urlString) else { completionHandler(.failure(AzureError.client("Invalid URL constructed for real-time notifications settings."))) return } var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let session = URLSession.shared session.dataTask(with: request) { data, response, error in if let error = error { completionHandler(.failure(AzureError.service("Error", error))) return } guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { completionHandler(.failure(AzureError.client("Received non-200 response from server."))) return } guard let data = data else { completionHandler(.failure(AzureError.client("No data received from the server."))) return } do { let decoder = JSONDecoder() let trouterSettings = try decoder.decode(RealTimeNotificationConfiguration.self, from: data) completionHandler(.success(trouterSettings)) } catch { completionHandler(.failure(AzureError.client("Failed to decode trouter settings: \(error.localizedDescription)"))) } }.resume() } } class CommunicationSkypeTokenProvider: NSObject, TrouterSkypetokenProvider { /// The cached skypeToken. var token: String /// The CommunicationTokenCredential. var credential: CommunicationTokenCredential /// Called from getSkypetoken, handle token errors here. var tokenRefreshHandler: TrouterTokenRefreshHandler /// Current number of token fetch retries. var tokenRetries: Int /// Max number of token retries allowed. let maxTokenRetries: Int = 3 /// Return the cached token, will attempt to refresh the token if forceRefresh is true. func getSkypetoken(_ forceRefresh: Bool) -> String! { if forceRefresh { tokenRetries += 1 // We have to return the token but don't attempt to refresh again // Pass true to the callback to signal that we should stop the connection if tokenRetries > maxTokenRetries { tokenRefreshHandler(true, nil) return token } // Fetch new token credential.token { token, error in // Let callback know we are attempting to refresh self.tokenRefreshHandler(false, error) guard let newToken = token?.token else { return } // Cache the new token self.token = newToken } } else { tokenRetries = 0 } return token } /// Initialize CommunicationSkypetokenProvider /// - Parameters: /// - token: The token to cache. /// - credential: CommunicationTokenCredential for refreshing the token. /// - tokenRefreshHandler: Called when the token is expired, stopSignalingClient will be true if retry attempts /// are exceeded. init( token: String, credential: CommunicationTokenCredential, tokenRefreshHandler: @escaping TrouterTokenRefreshHandler ) { self.token = token self.credential = credential self.tokenRefreshHandler = tokenRefreshHandler self.tokenRetries = 0 } } class CommunicationHandler: NSObject, TrouterListener { var handler: TrouterEventHandler var logger: ClientLogger init( handler: @escaping TrouterEventHandler, logger: ClientLogger = ClientLoggers.default(tag: "AzureCommunicationHandler") ) { self.handler = handler self.logger = logger } // MARK: TrouterListenerProtocol func onTrouterConnected(_: String, _: TrouterConnectionInfo) { do { logger.info("Trouter Connected") let chatEvent = try TrouterEventUtil.create(chatEvent: ChatEventId.realTimeNotificationConnected, from: nil) handler(chatEvent) } catch { logger.error("Error: \(error)") } } func onTrouterDisconnected() { do { logger.info("Trouter Disconnected") let chatEvent = try TrouterEventUtil.create( chatEvent: ChatEventId.realTimeNotificationDisconnected, from: nil ) handler(chatEvent) } catch { logger.error("Error: \(error)") } } func onTrouterRequest(_ request: TrouterRequest, _ response: TrouterResponse) { logger.info("Received a Trouter request \n") do { guard let requestJsonData = request.body.data(using: .utf8) else { throw AzureError.client("Unable to convert request body to Data.") } let generalPayload = try JSONDecoder().decode(BasePayload.self, from: requestJsonData) let chatEventId = try ChatEventId(forCode: generalPayload.eventId) // Convert trouter payload to chat event payload let chatEvent = try TrouterEventUtil.create(chatEvent: chatEventId, from: request) handler(chatEvent) } catch { logger.error("Error: \(error)") } // Notify Trouter the request was handled response.body = "Request has been handled" response.status = 200 let result: TrouterSendResponseResult = response.send() logger.info("Sent a response to Trouter: \(result)") } }