Skip to content

Commit

Permalink
Extended device info (PSG-772) (#6766)
Browse files Browse the repository at this point in the history
  • Loading branch information
ismailgulek authored Sep 29, 2022
1 parent 46a975b commit 2f689f4
Show file tree
Hide file tree
Showing 17 changed files with 825 additions and 156 deletions.
8 changes: 7 additions & 1 deletion Riot/Assets/en.lproj/Vector.strings
Original file line number Diff line number Diff line change
Expand Up @@ -2398,12 +2398,18 @@ To enable access, tap Settings> Location and select Always";

"user_session_details_title" = "Session details";
"user_session_details_session_section_header" = "Session";
"user_session_details_application_section_header" = "Application";
"user_session_details_device_section_header" = "Device";
"user_session_details_session_name" = "Session name";
"user_session_details_session_id" = "Session ID";
"user_session_details_session_section_footer" = "Copy any data by tapping on it and holding it down.";
"user_session_details_device_ip_address" = "IP address";

"user_session_details_device_ip_location" = "IP location";
"user_session_details_device_model" = "Model";
"user_session_details_device_os" = "Operating System";
"user_session_details_application_name" = "Name";
"user_session_details_application_version" = "Version";
"user_session_details_application_url" = "URL";
"user_session_overview_current_session_title" = "Current session";
"user_session_overview_session_title" = "Session";
"user_session_overview_session_details_button_title" = "Session details";
Expand Down
28 changes: 28 additions & 0 deletions Riot/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8471,10 +8471,38 @@ public class VectorL10n: NSObject {
public static var userIdTitle: String {
return VectorL10n.tr("Vector", "user_id_title")
}
/// Name
public static var userSessionDetailsApplicationName: String {
return VectorL10n.tr("Vector", "user_session_details_application_name")
}
/// Application
public static var userSessionDetailsApplicationSectionHeader: String {
return VectorL10n.tr("Vector", "user_session_details_application_section_header")
}
/// URL
public static var userSessionDetailsApplicationUrl: String {
return VectorL10n.tr("Vector", "user_session_details_application_url")
}
/// Version
public static var userSessionDetailsApplicationVersion: String {
return VectorL10n.tr("Vector", "user_session_details_application_version")
}
/// IP address
public static var userSessionDetailsDeviceIpAddress: String {
return VectorL10n.tr("Vector", "user_session_details_device_ip_address")
}
/// IP location
public static var userSessionDetailsDeviceIpLocation: String {
return VectorL10n.tr("Vector", "user_session_details_device_ip_location")
}
/// Model
public static var userSessionDetailsDeviceModel: String {
return VectorL10n.tr("Vector", "user_session_details_device_model")
}
/// Operating System
public static var userSessionDetailsDeviceOs: String {
return VectorL10n.tr("Vector", "user_session_details_device_os")
}
/// Device
public static var userSessionDetailsDeviceSectionHeader: String {
return VectorL10n.tr("Vector", "user_session_details_device_section_header")
Expand Down
24 changes: 24 additions & 0 deletions RiotSwiftUI/Modules/Common/Extensions/Collection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// 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 Foundation

extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}
201 changes: 201 additions & 0 deletions RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
//
// 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 Foundation

struct UserAgent {
let deviceType: DeviceType
let deviceModel: String?
let deviceOS: String?
let clientName: String?
let clientVersion: String?

static let unknown = UserAgent(deviceType: .unknown,
deviceModel: nil,
deviceOS: nil,
clientName: nil,
clientVersion: nil)
}

extension UserAgent: Equatable { }

enum UserAgentParser {
private enum Constants {
static let deviceInfoRegexPattern = "\\((?:[^)(]+|\\((?:[^)(]+|\\([^)(]*\\))*\\))*\\)"

static let androidKeyword = "; MatrixAndroidSdk2"
static let iosKeyword = "; iOS "
static let desktopKeyword = " Electron/"
static let webKeyword = "Mozilla/"
}

static func parse(_ userAgent: String) -> UserAgent {
if userAgent.vc_caseInsensitiveContains(Constants.androidKeyword) {
return parseAndroid(userAgent)
} else if userAgent.vc_caseInsensitiveContains(Constants.iosKeyword) {
return parseIOS(userAgent)
} else if userAgent.vc_caseInsensitiveContains(Constants.desktopKeyword) {
return parseDesktop(userAgent)
} else if userAgent.vc_caseInsensitiveContains(Constants.webKeyword) {
return parseWeb(userAgent)
}
return .unknown
}

// Legacy: Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)
// New: Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0)
private static func parseAndroid(_ userAgent: String) -> UserAgent {
var deviceModel: String?
var deviceOS: String?
var clientName: String?
var clientVersion: String?

let (beforeSlash, afterSlash) = userAgent.splitByFirst("/")
clientName = beforeSlash
if let afterSlash = afterSlash {
let (beforeSpace, afterSpace) = afterSlash.splitByFirst(" ")
clientVersion = beforeSpace
if let afterSpace = afterSpace {
if let deviceInfo = findFirstDeviceInfo(in: afterSpace) {
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ")
let isLegacy = deviceInfoComponents[safe: 0] == "Linux"
if isLegacy {
// find the segment starting with "Android"
if let osSegmentIndex = deviceInfoComponents.firstIndex(where: { $0.hasPrefix("Android") }) {
deviceOS = deviceInfoComponents[safe: osSegmentIndex]
deviceModel = deviceInfoComponents[safe: osSegmentIndex + 1]
}
} else {
deviceModel = deviceInfoComponents[safe: 0]
deviceOS = deviceInfoComponents[safe: 1]
}
}
}
}

return UserAgent(deviceType: .mobile,
deviceModel: deviceModel,
deviceOS: deviceOS,
clientName: clientName,
clientVersion: clientVersion)
}

// Legacy: Riot/1.8.21 (iPhone; iOS 15.2; Scale/3.00)
// New: Riot/1.8.21 (iPhone X; iOS 15.2; Scale/3.00)
private static func parseIOS(_ userAgent: String) -> UserAgent {
var deviceModel: String?
var deviceOS: String?
var clientName: String?
var clientVersion: String?

let (beforeSlash, afterSlash) = userAgent.splitByFirst("/")
clientName = beforeSlash
if let afterSlash = afterSlash {
let (beforeSpace, afterSpace) = afterSlash.splitByFirst(" ")
clientVersion = beforeSpace
if let afterSpace = afterSpace {
if let deviceInfo = findFirstDeviceInfo(in: afterSpace) {
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ")
deviceModel = deviceInfoComponents[safe: 0]
deviceOS = deviceInfoComponents[safe: 1]
}
}
}

return UserAgent(deviceType: .mobile,
deviceModel: deviceModel,
deviceOS: deviceOS,
clientName: clientName,
clientVersion: clientVersion)
}

// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36
private static func parseDesktop(_ userAgent: String) -> UserAgent {
var deviceOS: String?
let browserName = browserName(for: userAgent)

if let deviceInfo = findFirstDeviceInfo(in: userAgent) {
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ")
deviceOS = deviceInfoComponents[safe: 1]?.hasPrefix("Android") == true ? deviceInfoComponents[safe: 1] : deviceInfoComponents.first
}

return UserAgent(deviceType: .desktop,
deviceModel: browserName,
deviceOS: deviceOS,
clientName: nil,
clientVersion: nil)
}

// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
private static func parseWeb(_ userAgent: String) -> UserAgent {
let desktopUserAgent = parseDesktop(userAgent)

return UserAgent(deviceType: .web,
deviceModel: desktopUserAgent.deviceModel,
deviceOS: desktopUserAgent.deviceOS,
clientName: desktopUserAgent.clientName,
clientVersion: desktopUserAgent.clientVersion)
}

private static func findFirstDeviceInfo(in string: String) -> String? {
guard let regex = try? NSRegularExpression(pattern: Constants.deviceInfoRegexPattern,
options: .caseInsensitive) else {
return nil
}
var range = regex.rangeOfFirstMatch(in: string, range: NSRange(string.startIndex..., in: string))
if range.location != NSNotFound {
range.location += 1
range.length -= 2
return string[range]
}
return nil
}

private static func browserName(for userAgent: String) -> String? {
let components = userAgent.components(separatedBy: " ")
if components.last?.hasPrefix("Firefox") == true {
return "Firefox"
} else if components.last?.hasPrefix("Safari") == true
&& components[safe:components.count - 2]?.hasPrefix("Mobile") == true {
// mobile browser
let possibleBrowserName = components[safe:components.count - 3]?.components(separatedBy: "/").first
return possibleBrowserName == "Version" ? "Safari" : possibleBrowserName
} else if components.last?.hasPrefix("Safari") == true && components[safe:components.count - 2]?.hasPrefix("Version") == true {
return "Safari"
} else {
// regular browser
return components[safe:components.count - 2]?.components(separatedBy: "/").first
}
}
}

private extension String {
subscript(_ range: NSRange) -> String {
let start = index(startIndex, offsetBy: range.lowerBound)
let end = index(startIndex, offsetBy: range.upperBound)
let subString = self[start..<end]
return String(subString)
}

func splitByFirst(_ delimiter: Character) -> (String?, String?) {
guard let delimiterIndex = firstIndex(of: delimiter) else {
return (nil, nil)
}
let before = String(prefix(upTo: delimiterIndex))
let after = String(suffix(from: index(after: delimiterIndex)))
return (before, after)
}
}
31 changes: 28 additions & 3 deletions RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,41 @@ struct UserSessionInfo: Identifiable {

/// Last time the session was active
let lastSeenTimestamp: TimeInterval?


// MARK: - Application Properties

/// Application name used by the session
let applicationName: String?

/// Application version used by the session
let applicationVersion: String?

/// Application URL used by the session. Only applicable for web sessions.
let applicationURL: String?

// MARK: - Device Properties

/// Device model
let deviceModel: String?

/// Device OS
let deviceOS: String?

/// Last seen IP location
let lastSeenIPLocation: String?

/// Device name
let deviceName: String?

/// True to indicate that session has been used under `inactiveSessionDurationTreshold` value
let isActive: Bool

/// True to indicate that this is current user session
let isCurrent: Bool
}

extension UserSessionInfo: Equatable {
static func == (lhs: UserSessionInfo, rhs: UserSessionInfo) -> Bool {
return lhs.id == rhs.id
lhs.id == rhs.id
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,17 +134,23 @@ struct UserSessionCardViewPreview: View {
@Environment(\.theme) var theme: ThemeSwiftUI

let viewData: UserSessionCardViewData
init(isCurrentSessionInfo: Bool = false) {

init(isCurrent: Bool = false) {
let session = UserSessionInfo(id: "alice",
name: "iOS",
deviceType: .mobile,
isVerified: false,
lastSeenIP: "10.0.0.10",
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
lastSeenTimestamp: nil,
applicationName: "Element iOS",
applicationVersion: "1.0.0",
applicationURL: nil,
deviceModel: nil,
deviceOS: "iOS 15.5",
lastSeenIPLocation: nil,
deviceName: "My iPhone",
isActive: true,
isCurrent: isCurrentSessionInfo)

isCurrent: isCurrent)
viewData = UserSessionCardViewData(session: session)
}

Expand All @@ -161,8 +167,8 @@ struct UserSessionCardViewPreview: View {
struct UserSessionCardView_Previews: PreviewProvider {
static var previews: some View {
Group {
UserSessionCardViewPreview(isCurrentSessionInfo: true).theme(.light).preferredColorScheme(.light)
UserSessionCardViewPreview(isCurrentSessionInfo: true).theme(.dark).preferredColorScheme(.dark)
UserSessionCardViewPreview(isCurrent: true).theme(.light).preferredColorScheme(.light)
UserSessionCardViewPreview(isCurrent: true).theme(.dark).preferredColorScheme(.dark)
UserSessionCardViewPreview().theme(.light).preferredColorScheme(.light)
UserSessionCardViewPreview().theme(.dark).preferredColorScheme(.dark)
}
Expand Down
Loading

0 comments on commit 2f689f4

Please sign in to comment.