// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Combine import MatrixSDK class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol { /// Delay after which session is considered inactive, 90 days private static let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400 private let dataProvider: UserSessionsDataProviderProtocol private var cancellables: Set = [] private(set) var overviewDataPublisher: CurrentValueSubject private(set) var sessionInfos: [UserSessionInfo] init(dataProvider: UserSessionsDataProviderProtocol) { self.dataProvider = dataProvider overviewDataPublisher = .init(UserSessionsOverviewData(currentSession: nil, unverifiedSessions: [], inactiveSessions: [], otherSessions: [], linkDeviceEnabled: false)) sessionInfos = [] setupInitialOverviewData() listenForSessionUpdates() } // MARK: - Public func updateOverviewData(completion: @escaping (Result) -> Void) { dataProvider.devices { response in switch response { case .success(let devices): self.sessionInfos = self.sortedSessionInfos(from: devices) Task { @MainActor in let linkDeviceEnabled = try? await self.dataProvider.qrLoginAvailable() let overviewData = self.sessionsOverviewData(from: self.sessionInfos, linkDeviceEnabled: linkDeviceEnabled ?? false) self.overviewDataPublisher.send(overviewData) completion(.success(overviewData)) } case .failure(let error): completion(.failure(error)) } } } func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? { if currentSession?.id == sessionId { return currentSession } return otherSessions.first(where: { $0.id == sessionId }) } // MARK: - Private private func listenForSessionUpdates() { NotificationCenter.default.publisher(for: .MXDeviceInfoTrustLevelDidChange) .sink { [weak self] _ in self?.updateOverviewData { _ in } } .store(in: &cancellables) NotificationCenter.default.publisher(for: .MXDeviceListDidUpdateUsersDevices) .sink { [weak self] _ in self?.updateOverviewData { _ in } } .store(in: &cancellables) NotificationCenter.default.publisher(for: .MXCrossSigningInfoTrustLevelDidChange) .sink { [weak self] _ in self?.updateOverviewData { _ in } } .store(in: &cancellables) } private func setupInitialOverviewData() { guard let currentSessionInfo = getCurrentSessionInfo() else { return } overviewDataPublisher = .init(UserSessionsOverviewData(currentSession: currentSessionInfo, unverifiedSessions: currentSessionInfo.verificationState == .verified ? [] : [currentSessionInfo], inactiveSessions: currentSessionInfo.isActive ? [] : [currentSessionInfo], otherSessions: [], linkDeviceEnabled: false)) } private func getCurrentSessionInfo() -> UserSessionInfo? { guard let mainAccount = dataProvider.activeAccounts.first, let device = mainAccount.device else { return nil } return sessionInfo(from: device, isCurrentSession: true) } private func sortedSessionInfos(from devices: [MXDevice]) -> [UserSessionInfo] { devices .sorted { $0.lastSeenTs > $1.lastSeenTs } .map { sessionInfo(from: $0, isCurrentSession: $0.deviceId == dataProvider.myDeviceId) } } private func sessionsOverviewData(from allSessions: [UserSessionInfo], linkDeviceEnabled: Bool) -> UserSessionsOverviewData { UserSessionsOverviewData(currentSession: allSessions.filter(\.isCurrent).first, unverifiedSessions: allSessions.filter { $0.verificationState == .unverified && !$0.isCurrent }, inactiveSessions: allSessions.filter { !$0.isActive }, otherSessions: allSessions.filter { !$0.isCurrent }, linkDeviceEnabled: linkDeviceEnabled) } private func sessionInfo(from device: MXDevice, isCurrentSession: Bool) -> UserSessionInfo { let deviceInfo = deviceInfo(for: device.deviceId) let verificationState = dataProvider.verificationState(for: deviceInfo) let eventType = kMXAccountDataTypeClientInformation + "." + device.deviceId let appData = dataProvider.accountData(for: eventType) var userAgent: UserAgent? var isSessionActive = true if let lastSeenUserAgent = device.lastSeenUserAgent { userAgent = UserAgentParser.parse(lastSeenUserAgent) } if device.lastSeenTs > 0 { let elapsedTime = Date().timeIntervalSince1970 - TimeInterval(device.lastSeenTs / 1000) isSessionActive = elapsedTime < Self.inactiveSessionDurationTreshold } return UserSessionInfo(withDevice: device, applicationData: appData as? [String: String], userAgent: userAgent, verificationState: verificationState, isActive: isSessionActive, isCurrent: isCurrentSession) } private func deviceInfo(for deviceId: String) -> MXDeviceInfo? { guard let userId = dataProvider.myUserId else { return nil } return dataProvider.device(withDeviceId: deviceId, ofUser: userId) } } extension UserSessionInfo { init(withDevice device: MXDevice, applicationData: [String: String]?, userAgent: UserAgent?, verificationState: VerificationState, isActive: Bool, isCurrent: Bool) { self.init(id: device.deviceId, name: device.displayName, deviceType: userAgent?.deviceType ?? .unknown, verificationState: verificationState, lastSeenIP: device.lastSeenIp, lastSeenTimestamp: device.lastSeenTs > 0 ? TimeInterval(device.lastSeenTs / 1000) : nil, applicationName: applicationData?["name"], applicationVersion: applicationData?["version"], applicationURL: applicationData?["url"], deviceModel: userAgent?.deviceModel, deviceOS: userAgent?.deviceOS, lastSeenIPLocation: nil, clientName: userAgent?.clientName, clientVersion: userAgent?.clientVersion, isActive: isActive, isCurrent: isCurrent) } }