mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Merge pull request #5743 from vector-im/langleyd/5017_uisi_autoreporter
App: UISI AutoReporting
This commit is contained in:
commit
0f89eb014e
13 changed files with 658 additions and 107 deletions
|
@ -191,6 +191,7 @@ final class BuildSettings: NSObject {
|
|||
static let bugReportEndpointUrlString = "https://riot.im/bugreports"
|
||||
// Use the name allocated by the bug report server
|
||||
static let bugReportApplicationId = "riot-ios"
|
||||
static let bugReportUISIId = "element-auto-uisi"
|
||||
|
||||
|
||||
// MARK: - Integrations
|
||||
|
@ -379,6 +380,9 @@ final class BuildSettings: NSObject {
|
|||
// MARK: - Secrets Recovery
|
||||
static let secretsRecoveryAllowReset = true
|
||||
|
||||
// MARK: - UISI Autoreporting
|
||||
static let cryptoUISIAutoReportingEnabled = false
|
||||
|
||||
// MARK: - Polls
|
||||
|
||||
static var pollsEnabled: Bool {
|
||||
|
|
|
@ -634,6 +634,7 @@ Tap the + to start adding people.";
|
|||
"settings_labs_enable_ringing_for_group_calls" = "Ring for group calls";
|
||||
"settings_labs_enabled_polls" = "Polls";
|
||||
"settings_labs_enable_threads" = "Threaded messaging";
|
||||
"settings_labs_enable_auto_report_decryption_errors" = "Auto Report Decryption Errors";
|
||||
"settings_labs_use_only_latest_user_avatar_and_name" = "Show latest avatar and name for users in message history";
|
||||
|
||||
"settings_version" = "Version %@";
|
||||
|
@ -2435,7 +2436,7 @@ Tap the + to start adding people.";
|
|||
"language_picker_title" = "Choose a language";
|
||||
"language_picker_default_language" = "Default (%@)";
|
||||
|
||||
/* -*-
|
||||
/* -*-
|
||||
Automatic localization for en
|
||||
|
||||
The following key/value pairs were extracted from the android i18n file:
|
||||
|
@ -2518,17 +2519,17 @@ Tap the + to start adding people.";
|
|||
"notice_room_history_visible_to_members_from_joined_point_by_you" = "You made future room history visible to all room members, from the point they joined.";
|
||||
"notice_room_history_visible_to_members_from_joined_point_by_you_for_dm" = "You made future messages visible to everyone, from when they joined.";
|
||||
|
||||
// Room Screen
|
||||
// Room Screen
|
||||
|
||||
// general errors
|
||||
// general errors
|
||||
|
||||
// Home Screen
|
||||
// Home Screen
|
||||
|
||||
// Last seen time
|
||||
// Last seen time
|
||||
|
||||
// call events
|
||||
// call events
|
||||
|
||||
/* -*-
|
||||
/* -*-
|
||||
Automatic localization for en
|
||||
|
||||
The following key/value pairs were extracted from the android i18n file:
|
||||
|
@ -2536,9 +2537,9 @@ Tap the + to start adding people.";
|
|||
*/
|
||||
|
||||
|
||||
// titles
|
||||
// titles
|
||||
|
||||
// button names
|
||||
// button names
|
||||
"send" = "Send";
|
||||
"copy_button_name" = "Copy";
|
||||
"resend" = "Resend";
|
||||
|
@ -2546,7 +2547,7 @@ Tap the + to start adding people.";
|
|||
"share" = "Share";
|
||||
"delete" = "Delete";
|
||||
|
||||
// actions
|
||||
// actions
|
||||
"action_logout" = "Logout";
|
||||
"create_room" = "Create Room";
|
||||
"login" = "Login";
|
||||
|
@ -2561,32 +2562,32 @@ Tap the + to start adding people.";
|
|||
"unban" = "Un-ban";
|
||||
"message_unsaved_changes" = "There are unsaved changes. Leaving will discard them.";
|
||||
|
||||
// Login Screen
|
||||
// Login Screen
|
||||
"login_error_already_logged_in" = "Already logged in";
|
||||
"login_error_must_start_http" = "URL must start with http[s]://";
|
||||
|
||||
// members list Screen
|
||||
// members list Screen
|
||||
|
||||
// accounts list Screen
|
||||
// accounts list Screen
|
||||
|
||||
// image size selection
|
||||
// image size selection
|
||||
|
||||
// invitation members list Screen
|
||||
// invitation members list Screen
|
||||
|
||||
// room creation dialog Screen
|
||||
// room creation dialog Screen
|
||||
|
||||
// room info dialog Screen
|
||||
|
||||
// room details dialog screen
|
||||
|
||||
// contacts list screen
|
||||
// contacts list screen
|
||||
"invitation_message" = "I\'d like to chat with you with matrix. Please, visit the website http://matrix.org to have more information.";
|
||||
|
||||
// Settings screen
|
||||
// Settings screen
|
||||
"settings_title_config" = "Configuration";
|
||||
"settings_title_notifications" = "Notifications";
|
||||
|
||||
// Notification settings screen
|
||||
// Notification settings screen
|
||||
"notification_settings_disable_all" = "Disable all notifications";
|
||||
"notification_settings_enable_notifications" = "Enable notifications";
|
||||
"notification_settings_enable_notifications_warning" = "All notifications are currently disabled for all devices.";
|
||||
|
@ -2613,10 +2614,10 @@ Tap the + to start adding people.";
|
|||
"notification_settings_by_default" = "By default...";
|
||||
"notification_settings_notify_all_other" = "Notify for all other messages/rooms";
|
||||
|
||||
// gcm section
|
||||
// gcm section
|
||||
"settings_config_identity_server" = "Identity server: %@";
|
||||
|
||||
// Settings keys
|
||||
// Settings keys
|
||||
|
||||
// call string
|
||||
"call_connecting" = "Connecting…";
|
||||
|
@ -2649,4 +2650,3 @@ Tap the + to start adding people.";
|
|||
"ssl_unexpected_existing_expl" = "The certificate has changed from one that was trusted by your phone. This is HIGHLY UNUSUAL. It is recommended that you DO NOT ACCEPT this new certificate.";
|
||||
"ssl_expected_existing_expl" = "The certificate has changed from a previously trusted one to one that is not trusted. The server may have renewed its certificate. Contact the server administrator for the expected fingerprint.";
|
||||
"ssl_only_accept" = "ONLY accept the certificate if the server administrator has published a fingerprint that matches the one above.";
|
||||
|
||||
|
|
26
Riot/Categories/Codable.swift
Normal file
26
Riot/Categories/Codable.swift
Normal file
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// Copyright 2021 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 Encodable {
|
||||
/// Convenience method to get the json string of an Encodable
|
||||
var jsonString: String? {
|
||||
let encoder = JSONEncoder()
|
||||
guard let jsonData = try? encoder.encode(self) else { return nil }
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
}
|
||||
}
|
114
Riot/Categories/MXBugReportRestClient+Riot.swift
Normal file
114
Riot/Categories/MXBugReportRestClient+Riot.swift
Normal file
|
@ -0,0 +1,114 @@
|
|||
//
|
||||
// Copyright 2021 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
|
||||
import MatrixSDK
|
||||
import GBDeviceInfo
|
||||
|
||||
extension MXBugReportRestClient {
|
||||
|
||||
@objc static func vc_bugReportRestClient(appName: String) -> MXBugReportRestClient {
|
||||
let client = MXBugReportRestClient(bugReportEndpoint: BuildSettings.bugReportEndpointUrlString)
|
||||
// App info
|
||||
client.appName = appName
|
||||
client.version = AppDelegate.theDelegate().appVersion
|
||||
client.build = AppDelegate.theDelegate().build
|
||||
|
||||
client.deviceModel = GBDeviceInfo.deviceInfo().modelString
|
||||
client.deviceOS = "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)"
|
||||
return client
|
||||
}
|
||||
|
||||
@objc func vc_sendBugReport(
|
||||
description: String,
|
||||
sendLogs: Bool,
|
||||
sendCrashLog: Bool,
|
||||
sendFiles: [URL]? = nil,
|
||||
additionalLabels: [String]? = nil,
|
||||
customFields: [String: String]? = nil,
|
||||
progress: ((MXBugReportState, Progress?) -> Void)? = nil,
|
||||
success: ((String?) -> Void)? = nil,
|
||||
failure: ((Error?) -> Void)? = nil
|
||||
) {
|
||||
// User info (TODO: handle multi-account and find a way to expose them in rageshake API)
|
||||
var userInfo = [String: String]()
|
||||
let mainAccount = MXKAccountManager.shared().accounts.first
|
||||
if let userId = mainAccount?.mxSession.myUser.userId {
|
||||
userInfo["user_id"] = userId
|
||||
}
|
||||
if let deviceId = mainAccount?.mxSession.matrixRestClient.credentials.deviceId {
|
||||
userInfo["device_id"] = deviceId
|
||||
}
|
||||
|
||||
userInfo["locale"] = NSLocale.preferredLanguages[0]
|
||||
userInfo["default_app_language"] = Bundle.main.preferredLocalizations[0] // The language chosen by the OS
|
||||
userInfo["app_language"] = Bundle.mxk_language() ?? userInfo["default_app_language"] // The language chosen by the user
|
||||
|
||||
// Application settings
|
||||
userInfo["lazy_loading"] = MXKAppSettings.standard().syncWithLazyLoadOfRoomMembers ? "ON" : "OFF"
|
||||
|
||||
let currentDate = Date()
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
userInfo["local_time"] = dateFormatter.string(from: currentDate)
|
||||
|
||||
dateFormatter.timeZone = TimeZone(identifier: "UTC")
|
||||
userInfo["utc_time"] = dateFormatter.string(from: currentDate)
|
||||
|
||||
if let customFields = customFields {
|
||||
// combine userInfo with custom fields overriding with custom where there is a conflict
|
||||
userInfo.merge(customFields) { (_, new) in new }
|
||||
}
|
||||
others = userInfo
|
||||
|
||||
var labels: [String] = additionalLabels ?? [String]()
|
||||
// Add a Github label giving information about the version
|
||||
if var versionLabel = version, let buildLabel = build {
|
||||
|
||||
// If this is not the app store version, be more accurate on the build origin
|
||||
if buildLabel == VectorL10n.settingsConfigNoBuildInfo {
|
||||
// This is a debug session from Xcode
|
||||
versionLabel += "-debug"
|
||||
} else if !buildLabel.contains("master") {
|
||||
// This is a Jenkins build. Add the branch and the build number
|
||||
let buildString = buildLabel.replacingOccurrences(of: " ", with: "-")
|
||||
versionLabel += "-\(buildString)"
|
||||
}
|
||||
labels += [versionLabel]
|
||||
}
|
||||
if sendCrashLog {
|
||||
labels += ["crash"]
|
||||
}
|
||||
|
||||
var sendDescription = description
|
||||
if sendCrashLog,
|
||||
let crashLogFile = MXLogger.crashLog(),
|
||||
let crashLog = try? String(contentsOfFile: crashLogFile, encoding: .utf8) {
|
||||
// Append the crash dump to the user description in order to ease triaging of GH issues
|
||||
sendDescription += "\n\n\n--------------------------------------------------------------------------------\n\n\(crashLog)"
|
||||
}
|
||||
|
||||
sendBugReport(sendDescription,
|
||||
sendLogs: sendLogs,
|
||||
sendCrashLog: sendCrashLog,
|
||||
sendFiles: sendFiles,
|
||||
attachGitHubLabels: labels,
|
||||
progress: progress,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
|
||||
}
|
37
Riot/Categories/Publisher+Riot.swift
Normal file
37
Riot/Categories/Publisher+Riot.swift
Normal file
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension Publisher {
|
||||
|
||||
///
|
||||
/// Buffer upstream items and guarantee a time interval spacing out the published items.
|
||||
/// - Parameters:
|
||||
/// - spacingDelay: A delay in seconds to guarantee between emissions
|
||||
/// - scheduler: The `DispatchQueue` on which to schedule emissions.
|
||||
/// - Returns: The new wrapped publisher
|
||||
func bufferAndSpace(spacingDelay: Int, scheduler: DispatchQueue = DispatchQueue.main) -> Publishers.FlatMap<
|
||||
Publishers.SetFailureType<Publishers.Delay<Just<Publishers.Buffer<Self>.Output>, DispatchQueue>, Publishers.Buffer<Self>.Failure>,
|
||||
Publishers.Buffer<Self>
|
||||
> {
|
||||
return buffer(size: .max, prefetch: .byRequest, whenFull: .dropNewest)
|
||||
.flatMap(maxPublishers: .max(1)) {
|
||||
Just($0).delay(for: .seconds(spacingDelay), scheduler: scheduler)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6699,6 +6699,10 @@ public class VectorL10n: NSObject {
|
|||
public static var settingsLabsE2eEncryptionPromptMessage: String {
|
||||
return VectorL10n.tr("Vector", "settings_labs_e2e_encryption_prompt_message")
|
||||
}
|
||||
/// Auto Report Decryption Errors
|
||||
public static var settingsLabsEnableAutoReportDecryptionErrors: String {
|
||||
return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors")
|
||||
}
|
||||
/// Ring for group calls
|
||||
public static var settingsLabsEnableRingingForGroupCalls: String {
|
||||
return VectorL10n.tr("Vector", "settings_labs_enable_ringing_for_group_calls")
|
||||
|
|
29
Riot/Managers/Settings/RiotSettings+Publisher.swift
Normal file
29
Riot/Managers/Settings/RiotSettings+Publisher.swift
Normal file
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// Copyright 2021 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
|
||||
import Combine
|
||||
|
||||
extension RiotSettings {
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
func publisher(for key: String) -> AnyPublisher<Notification, Never> {
|
||||
return NotificationCenter.default.publisher(for: .userDefaultValueUpdated)
|
||||
.filter({ $0.object as? String == key })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
|
@ -30,6 +30,7 @@ final class RiotSettings: NSObject {
|
|||
static let pinRoomsWithMissedNotificationsOnHome = "pinRoomsWithMissedNotif"
|
||||
static let pinRoomsWithUnreadMessagesOnHome = "pinRoomsWithUnread"
|
||||
static let showAllRoomsInHomeSpace = "showAllRoomsInHomeSpace"
|
||||
static let enableUISIAutoReporting = "enableUISIAutoReporting"
|
||||
}
|
||||
|
||||
static let shared = RiotSettings()
|
||||
|
@ -146,6 +147,10 @@ final class RiotSettings: NSObject {
|
|||
@UserDefault(key: "enableThreads", defaultValue: false, storage: defaults)
|
||||
var enableThreads
|
||||
|
||||
/// Indicates if auto reporting of decryption errors is enabled
|
||||
@UserDefault(key: UserDefaultsKeys.enableUISIAutoReporting, defaultValue: BuildSettings.cryptoUISIAutoReportingEnabled, storage: defaults)
|
||||
var enableUISIAutoReporting
|
||||
|
||||
// MARK: Calls
|
||||
|
||||
/// Indicate if `allowStunServerFallback` settings has been set once.
|
||||
|
|
236
Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift
Normal file
236
Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift
Normal file
|
@ -0,0 +1,236 @@
|
|||
//
|
||||
// Copyright 2021 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
|
||||
import MatrixSDK
|
||||
import Combine
|
||||
|
||||
struct UISIAutoReportData {
|
||||
let eventId: String?
|
||||
let roomId: String?
|
||||
let senderKey: String?
|
||||
let deviceId: String?
|
||||
let userId: String?
|
||||
let sessionId: String?
|
||||
}
|
||||
|
||||
extension UISIAutoReportData: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case eventId = "event_id"
|
||||
case roomId = "room_id"
|
||||
case senderKey = "sender_key"
|
||||
case deviceId = "device_id"
|
||||
case userId = "user_id"
|
||||
case sessionId = "session_id"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Listens for failed decryption events and silently sends reports RageShake server.
|
||||
/// Also requests that message senders send a matching report to have both sides of the interaction.
|
||||
@available(iOS 14.0, *)
|
||||
@objcMembers class UISIAutoReporter: NSObject, UISIDetectorDelegate {
|
||||
|
||||
struct ReportInfo: Hashable {
|
||||
let roomId: String
|
||||
let sessionId: String
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private static let autoRsRequest = "im.vector.auto_rs_request"
|
||||
private static let reportSpacing = 60
|
||||
|
||||
private let bugReporter: MXBugReportRestClient
|
||||
private let dispatchQueue = DispatchQueue(label: "io.element.UISIAutoReporter.queue")
|
||||
// Simple in memory cache of already sent report
|
||||
private var alreadyReportedUisi = Set<ReportInfo>()
|
||||
private let e2eDetectedSubject = PassthroughSubject<UISIDetectedMessage, Never>()
|
||||
private let matchingRSRequestSubject = PassthroughSubject<MXEvent, Never>()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var sessions = [MXSession]()
|
||||
private var enabled = false {
|
||||
didSet {
|
||||
guard oldValue != enabled else { return }
|
||||
detector.enabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
override init() {
|
||||
self.bugReporter = MXBugReportRestClient.vc_bugReportRestClient(appName: BuildSettings.bugReportUISIId)
|
||||
super.init()
|
||||
// Simple rate limiting, for any rage-shakes emitted we guarantee a spacing between requests.
|
||||
e2eDetectedSubject
|
||||
.bufferAndSpace(spacingDelay: Self.reportSpacing)
|
||||
.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.sendRageShake(source: $0)
|
||||
}.store(in: &cancellables)
|
||||
|
||||
matchingRSRequestSubject
|
||||
.bufferAndSpace(spacingDelay: Self.reportSpacing)
|
||||
.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.sendMatchingRageShake(source: $0)
|
||||
}.store(in: &cancellables)
|
||||
|
||||
self.enabled = RiotSettings.shared.enableUISIAutoReporting
|
||||
RiotSettings.shared.publisher(for: RiotSettings.UserDefaultsKeys.enableUISIAutoReporting)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.enabled = RiotSettings.shared.enableUISIAutoReporting
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private lazy var detector: UISIDetector = {
|
||||
let detector = UISIDetector()
|
||||
detector.delegate = self
|
||||
return detector
|
||||
}()
|
||||
|
||||
var reciprocateToDeviceEventType: String {
|
||||
return Self.autoRsRequest
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func uisiDetected(source: UISIDetectedMessage) {
|
||||
dispatchQueue.async {
|
||||
let reportInfo = ReportInfo(roomId: source.roomId, sessionId: source.sessionId)
|
||||
let alreadySent = self.alreadyReportedUisi.contains(reportInfo)
|
||||
if !alreadySent {
|
||||
self.alreadyReportedUisi.insert(reportInfo)
|
||||
self.e2eDetectedSubject.send(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func add(_ session: MXSession) {
|
||||
sessions.append(session)
|
||||
detector.enabled = enabled
|
||||
session.eventStreamService.add(eventStreamListener: detector)
|
||||
}
|
||||
|
||||
func remove(_ session: MXSession) {
|
||||
if let index = sessions.firstIndex(of: session) {
|
||||
sessions.remove(at: index)
|
||||
}
|
||||
session.eventStreamService.remove(eventStreamListener: detector)
|
||||
}
|
||||
|
||||
func uisiReciprocateRequest(source: MXEvent) {
|
||||
guard source.type == Self.autoRsRequest else { return }
|
||||
self.matchingRSRequestSubject.send(source)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func sendRageShake(source: UISIDetectedMessage) {
|
||||
MXLog.debug("[UISIAutoReporter] sendRageShake")
|
||||
guard let session = sessions.first else { return }
|
||||
let uisiData = UISIAutoReportData(
|
||||
eventId: source.eventId,
|
||||
roomId: source.roomId,
|
||||
senderKey: source.senderKey,
|
||||
deviceId: source.senderDeviceId,
|
||||
userId: source.senderUserId,
|
||||
sessionId: source.sessionId
|
||||
).jsonString ?? ""
|
||||
|
||||
self.bugReporter.vc_sendBugReport(
|
||||
description: "Auto-reporting decryption error",
|
||||
sendLogs: true,
|
||||
sendCrashLog: true,
|
||||
additionalLabels: [
|
||||
"Z-UISI",
|
||||
"ios",
|
||||
"uisi-recipient"
|
||||
],
|
||||
customFields: ["auto_uisi": uisiData],
|
||||
success: { reportUrl in
|
||||
let contentMap = MXUsersDevicesMap<NSDictionary>()
|
||||
let content = [
|
||||
"event_id": source.eventId,
|
||||
"room_id": source.roomId,
|
||||
"session_id": source.sessionId,
|
||||
"device_id": source.senderDeviceId,
|
||||
"user_id": source.senderUserId,
|
||||
"sender_key": source.senderKey,
|
||||
"recipient_rageshake": reportUrl
|
||||
]
|
||||
contentMap.setObject(content as NSDictionary, forUser: source.senderUserId, andDevice: source.senderDeviceId)
|
||||
session.matrixRestClient.sendDirectToDevice(
|
||||
eventType: Self.autoRsRequest,
|
||||
contentMap: contentMap,
|
||||
txnId: nil
|
||||
) { response in
|
||||
if response.isFailure {
|
||||
MXLog.warning("failed to send auto-uisi to device")
|
||||
}
|
||||
}
|
||||
},
|
||||
failure: { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
self.dispatchQueue.async {
|
||||
self.alreadyReportedUisi.remove(ReportInfo(roomId: source.roomId, sessionId: source.sessionId))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func sendMatchingRageShake(source: MXEvent) {
|
||||
MXLog.debug("[UISIAutoReporter] sendMatchingRageShake")
|
||||
let eventId = source.content["event_id"] as? String
|
||||
let roomId = source.content["room_id"] as? String
|
||||
let sessionId = source.content["session_id"] as? String
|
||||
let deviceId = source.content["device_id"] as? String
|
||||
let userId = source.content["user_id"] as? String
|
||||
let senderKey = source.content["sender_key"] as? String
|
||||
let matchingIssue = source.content["recipient_rageshake"] as? String
|
||||
|
||||
var description = "Auto-reporting decryption error (sender)"
|
||||
if let matchingIssue = matchingIssue {
|
||||
description += "\nRecipient rageshake: \(matchingIssue)"
|
||||
}
|
||||
|
||||
let uisiData = UISIAutoReportData(
|
||||
eventId: eventId,
|
||||
roomId: roomId,
|
||||
senderKey: senderKey,
|
||||
deviceId: deviceId,
|
||||
userId: userId,
|
||||
sessionId: sessionId
|
||||
).jsonString ?? ""
|
||||
|
||||
self.bugReporter.vc_sendBugReport(
|
||||
description: description,
|
||||
sendLogs: true,
|
||||
sendCrashLog: true,
|
||||
additionalLabels: [
|
||||
"Z-UISI",
|
||||
"ios",
|
||||
"uisi-sender"
|
||||
],
|
||||
customFields: [
|
||||
"auto_uisi": uisiData,
|
||||
"recipient_rageshake": matchingIssue ?? ""
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
}
|
115
Riot/Managers/UISIAutoReporter/UISIDetector.swift
Normal file
115
Riot/Managers/UISIAutoReporter/UISIDetector.swift
Normal file
|
@ -0,0 +1,115 @@
|
|||
//
|
||||
// Copyright 2021 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 MatrixSDK
|
||||
import Foundation
|
||||
|
||||
protocol UISIDetectorDelegate: AnyObject {
|
||||
var reciprocateToDeviceEventType: String { get }
|
||||
func uisiDetected(source: UISIDetectedMessage)
|
||||
func uisiReciprocateRequest(source: MXEvent)
|
||||
}
|
||||
|
||||
struct UISIDetectedMessage {
|
||||
let eventId: String
|
||||
let roomId: String
|
||||
let senderUserId: String
|
||||
let senderDeviceId: String
|
||||
let senderKey: String
|
||||
let sessionId: String
|
||||
|
||||
static func fromEvent(event: MXEvent) -> UISIDetectedMessage {
|
||||
return UISIDetectedMessage(
|
||||
eventId: event.eventId ?? "",
|
||||
roomId: event.roomId,
|
||||
senderUserId: event.sender,
|
||||
senderDeviceId: event.wireContent["device_id"] as? String ?? "",
|
||||
senderKey: event.wireContent["sender_key"] as? String ?? "",
|
||||
sessionId: event.wireContent["session_id"] as? String ?? ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Detects decryption errors that occur and don't recover within a grace period.
|
||||
/// see `UISIDetectorDelegate` for listening to detections.
|
||||
class UISIDetector: MXLiveEventListener {
|
||||
|
||||
weak var delegate: UISIDetectorDelegate?
|
||||
var enabled = false
|
||||
|
||||
var initialSyncCompleted = false
|
||||
private var trackedUISIs = [String: DispatchSourceTimer]()
|
||||
private let dispatchQueue = DispatchQueue(label: "io.element.UISIDetector.queue")
|
||||
private static let gracePeriodSeconds = 30
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func onSessionStateChanged(state: MXSessionState) {
|
||||
dispatchQueue.async {
|
||||
self.initialSyncCompleted = state == .running
|
||||
}
|
||||
}
|
||||
|
||||
func onLiveEventDecryptionAttempted(event: MXEvent, result: MXEventDecryptionResult) {
|
||||
guard enabled, let eventId = event.eventId, let roomId = event.roomId else { return }
|
||||
dispatchQueue.async {
|
||||
let trackedId = Self.trackedEventId(roomId: eventId, eventId: roomId)
|
||||
|
||||
if let timer = self.trackedUISIs[trackedId],
|
||||
result.clearEvent != nil {
|
||||
// successfully decrypted during grace period, cancel timer.
|
||||
self.trackedUISIs[trackedId] = nil
|
||||
timer.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
guard self.initialSyncCompleted,
|
||||
result.clearEvent == nil
|
||||
else { return }
|
||||
|
||||
// track uisi and report it only if it is not decrypted before grade period ends
|
||||
let timer = DispatchSource.makeTimerSource(queue: self.dispatchQueue)
|
||||
timer.schedule(deadline: .now() + .seconds(Self.gracePeriodSeconds))
|
||||
timer.setEventHandler { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.trackedUISIs[trackedId] = nil
|
||||
MXLog.verbose("[UISIDetector] onLiveEventDecryptionAttempted: Timeout on \(eventId)")
|
||||
self.triggerUISI(source: UISIDetectedMessage.fromEvent(event: event))
|
||||
}
|
||||
self.trackedUISIs[trackedId] = timer
|
||||
timer.activate()
|
||||
}
|
||||
}
|
||||
|
||||
func onLiveToDeviceEvent(event: MXEvent) {
|
||||
guard enabled, event.type == delegate?.reciprocateToDeviceEventType else { return }
|
||||
delegate?.uisiReciprocateRequest(source: event)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func triggerUISI(source: UISIDetectedMessage) {
|
||||
guard enabled else { return }
|
||||
MXLog.info("[UISIDetector] triggerUISI: Unable To Decrypt \(source)")
|
||||
self.delegate?.uisiDetected(source: source)
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
private static func trackedEventId(roomId: String, eventId: String) -> String {
|
||||
return "\(roomId)-\(eventId)"
|
||||
}
|
||||
}
|
|
@ -225,6 +225,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
@property (nonatomic, strong) PushNotificationStore *pushNotificationStore;
|
||||
@property (nonatomic, strong) LocalAuthenticationService *localAuthenticationService;
|
||||
@property (nonatomic, strong, readwrite) CallPresenter *callPresenter;
|
||||
@property (nonatomic, strong, readwrite) id uisiAutoReporter;
|
||||
|
||||
@property (nonatomic, strong) MajorUpdateManager *majorUpdateManager;
|
||||
|
||||
|
@ -471,6 +472,10 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
|
||||
self.spaceFeatureUnavailablePresenter = [SpaceFeatureUnavailablePresenter new];
|
||||
|
||||
if (@available(iOS 14.0, *)) {
|
||||
self.uisiAutoReporter = [[UISIAutoReporter alloc] init];
|
||||
}
|
||||
|
||||
// Add matrix observers, and initialize matrix sessions if the app is not launched in background.
|
||||
[self initMatrixSessions];
|
||||
|
||||
|
@ -2152,6 +2157,17 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
// register the session to the call service
|
||||
[_callPresenter addMatrixSession:mxSession];
|
||||
|
||||
// register the session to the uisi auto-reporter
|
||||
if (_uisiAutoReporter != nil)
|
||||
{
|
||||
if (@available(iOS 14.0, *))
|
||||
{
|
||||
UISIAutoReporter* uisiAutoReporter = (UISIAutoReporter*)_uisiAutoReporter;
|
||||
[uisiAutoReporter add:mxSession];
|
||||
}
|
||||
}
|
||||
[_callPresenter addMatrixSession:mxSession];
|
||||
|
||||
[mxSessionArray addObject:mxSession];
|
||||
|
||||
// Do the one time check on device id
|
||||
|
@ -2167,6 +2183,16 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
|
||||
// remove session from the call service
|
||||
[_callPresenter removeMatrixSession:mxSession];
|
||||
|
||||
// register the session to the uisi auto-reporter
|
||||
if (_uisiAutoReporter != nil)
|
||||
{
|
||||
if (@available(iOS 14.0, *))
|
||||
{
|
||||
UISIAutoReporter* uisiAutoReporter = (UISIAutoReporter*)_uisiAutoReporter;
|
||||
[uisiAutoReporter remove:mxSession];
|
||||
}
|
||||
}
|
||||
|
||||
// Update the widgets manager
|
||||
[[WidgetManager sharedManager] removeMatrixSession:mxSession];
|
||||
|
|
|
@ -295,47 +295,8 @@
|
|||
{
|
||||
self.isSendingLogs = YES;
|
||||
|
||||
// Setup data to send
|
||||
bugReportRestClient = [[MXBugReportRestClient alloc] initWithBugReportEndpoint:BuildSettings.bugReportEndpointUrlString];
|
||||
|
||||
// App info
|
||||
bugReportRestClient.appName = BuildSettings.bugReportApplicationId;
|
||||
bugReportRestClient.version = [AppDelegate theDelegate].appVersion;
|
||||
bugReportRestClient.build = [AppDelegate theDelegate].build;
|
||||
|
||||
// Device info
|
||||
bugReportRestClient.deviceModel = [GBDeviceInfo deviceInfo].modelString;
|
||||
bugReportRestClient.deviceOS = [NSString stringWithFormat:@"%@ %@", [[UIDevice currentDevice] systemName], [[UIDevice currentDevice] systemVersion]];
|
||||
|
||||
// User info (TODO: handle multi-account and find a way to expose them in rageshake API)
|
||||
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
|
||||
MXKAccount *mainAccount = [MXKAccountManager sharedManager].accounts.firstObject;
|
||||
if (mainAccount.mxSession.myUser.userId)
|
||||
{
|
||||
userInfo[@"user_id"] = mainAccount.mxSession.myUser.userId;
|
||||
}
|
||||
if (mainAccount.mxSession.matrixRestClient.credentials.deviceId)
|
||||
{
|
||||
userInfo[@"device_id"] = mainAccount.mxSession.matrixRestClient.credentials.deviceId;
|
||||
}
|
||||
|
||||
userInfo[@"locale"] = [NSLocale preferredLanguages][0];
|
||||
userInfo[@"default_app_language"] = [[NSBundle mainBundle] preferredLocalizations][0]; // The language chosen by the OS
|
||||
userInfo[@"app_language"] = [NSBundle mxk_language] ? [NSBundle mxk_language] : userInfo[@"default_app_language"]; // The language chosen by the user
|
||||
|
||||
// Application settings
|
||||
userInfo[@"lazy_loading"] = [MXKAppSettings standardAppSettings].syncWithLazyLoadOfRoomMembers ? @"ON" : @"OFF";
|
||||
|
||||
NSDate *currentDate = [NSDate date];
|
||||
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||||
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
|
||||
userInfo[@"local_time"] = [dateFormatter stringFromDate:currentDate];
|
||||
|
||||
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
|
||||
userInfo[@"utc_time"] = [dateFormatter stringFromDate:currentDate];
|
||||
|
||||
bugReportRestClient.others = userInfo;
|
||||
|
||||
bugReportRestClient = [MXBugReportRestClient vc_bugReportRestClientWithAppName:BuildSettings.bugReportApplicationId];
|
||||
|
||||
// Screenshot
|
||||
NSArray<NSURL*> *files;
|
||||
if (_screenshot && _sendScreenshot)
|
||||
|
@ -347,56 +308,23 @@
|
|||
|
||||
files = @[screenShotFile];
|
||||
}
|
||||
|
||||
// Prepare labels to attach to the GitHub issue
|
||||
NSMutableArray<NSString*> *gitHubLabels = [NSMutableArray array];
|
||||
if (_reportCrash)
|
||||
{
|
||||
// Label the GH issue as "crash"
|
||||
[gitHubLabels addObject:@"crash"];
|
||||
}
|
||||
|
||||
// Add a Github label giving information about the version
|
||||
if (bugReportRestClient.version && bugReportRestClient.build)
|
||||
{
|
||||
NSString *build = bugReportRestClient.build;
|
||||
NSString *versionLabel = bugReportRestClient.version;
|
||||
|
||||
// If this is not the app store version, be more accurate on the build origin
|
||||
if ([build isEqualToString:[VectorL10n settingsConfigNoBuildInfo]])
|
||||
{
|
||||
// This is a debug session from Xcode
|
||||
versionLabel = [versionLabel stringByAppendingString:@"-debug"];
|
||||
}
|
||||
else if (build && ![build containsString:@"master"])
|
||||
{
|
||||
// This is a Jenkins build. Add the branch and the build number
|
||||
NSString *buildString = [build stringByReplacingOccurrencesOfString:@" " withString:@"-"];
|
||||
versionLabel = [[versionLabel stringByAppendingString:@"-"] stringByAppendingString:buildString];
|
||||
}
|
||||
|
||||
[gitHubLabels addObject:versionLabel];
|
||||
}
|
||||
|
||||
|
||||
NSMutableString *bugReportDescription = [NSMutableString stringWithString:_bugReportDescriptionTextView.text];
|
||||
|
||||
if (_reportCrash)
|
||||
{
|
||||
// Append the crash dump to the user description in order to ease triaging of GH issues
|
||||
NSString *crashLogFile = [MXLogger crashLog];
|
||||
NSString *crashLog = [NSString stringWithContentsOfFile:crashLogFile encoding:NSUTF8StringEncoding error:nil];
|
||||
[bugReportDescription appendFormat:@"\n\n\n--------------------------------------------------------------------------------\n\n%@", crashLog];
|
||||
}
|
||||
|
||||
|
||||
// starting a background task to have a bit of extra time in case of user forgets about the report and sends the app to background
|
||||
__block UIBackgroundTaskIdentifier operationBackgroundId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
|
||||
[[UIApplication sharedApplication] endBackgroundTask:operationBackgroundId];
|
||||
operationBackgroundId = UIBackgroundTaskInvalid;
|
||||
}];
|
||||
|
||||
// Submit
|
||||
[bugReportRestClient sendBugReport:bugReportDescription sendLogs:_sendLogs sendCrashLog:_reportCrash sendFiles:files attachGitHubLabels:gitHubLabels progress:^(MXBugReportState state, NSProgress *progress) {
|
||||
|
||||
[bugReportRestClient vc_sendBugReportWithDescription:bugReportDescription
|
||||
sendLogs:_sendLogs
|
||||
sendCrashLog:_reportCrash
|
||||
sendFiles:files
|
||||
additionalLabels:nil
|
||||
customFields:nil
|
||||
progress:^(MXBugReportState state, NSProgress *progress) {
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case MXBugReportStateProgressZipping:
|
||||
|
@ -413,7 +341,7 @@
|
|||
|
||||
self.sendingProgress.progress = progress.fractionCompleted;
|
||||
|
||||
} success:^{
|
||||
} success:^(NSString *reportUrl){
|
||||
|
||||
self->bugReportRestClient = nil;
|
||||
|
||||
|
|
|
@ -160,6 +160,7 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE)
|
|||
LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0,
|
||||
LABS_ENABLE_THREADS_INDEX,
|
||||
LABS_ENABLE_MESSAGE_BUBBLES_INDEX,
|
||||
LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS,
|
||||
LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX
|
||||
};
|
||||
|
||||
|
@ -573,6 +574,7 @@ TableViewSectionsDelegate>
|
|||
[sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX];
|
||||
[sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX];
|
||||
[sectionLabs addRowWithTag:LABS_ENABLE_MESSAGE_BUBBLES_INDEX];
|
||||
[sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS];
|
||||
[sectionLabs addRowWithTag:LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX];
|
||||
sectionLabs.headerTitle = [VectorL10n settingsLabs];
|
||||
if (sectionLabs.hasAnyRows)
|
||||
|
@ -1486,6 +1488,21 @@ TableViewSectionsDelegate>
|
|||
return labelAndSwitchCell;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)buildAutoReportDecryptionErrorsCellForTableView:(UITableView*)tableView
|
||||
atIndexPath:(NSIndexPath*)indexPath
|
||||
{
|
||||
MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
|
||||
|
||||
labelAndSwitchCell.mxkLabel.text = [VectorL10n settingsLabsEnableAutoReportDecryptionErrors];
|
||||
|
||||
labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.enableUISIAutoReporting;
|
||||
labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
|
||||
labelAndSwitchCell.mxkSwitch.enabled = YES;
|
||||
[labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableAutoReportDecryptionErrors:) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
return labelAndSwitchCell;
|
||||
}
|
||||
|
||||
#pragma mark - 3Pid Add
|
||||
|
||||
- (void)showAuthenticationIfNeededForAdding:(MX3PIDMedium)medium withSession:(MXSession*)session completion:(void (^)(NSDictionary* authParams))completion
|
||||
|
@ -2458,6 +2475,10 @@ TableViewSectionsDelegate>
|
|||
{
|
||||
cell = [self buildMessageBubblesCellForTableView:tableView atIndexPath:indexPath];
|
||||
}
|
||||
else if (row == LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS)
|
||||
{
|
||||
cell = [self buildAutoReportDecryptionErrorsCellForTableView:tableView atIndexPath:indexPath];
|
||||
}
|
||||
else if (row == LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX)
|
||||
{
|
||||
MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
|
||||
|
@ -3903,6 +3924,12 @@ TableViewSectionsDelegate>
|
|||
[roomDataSourceManager reset];
|
||||
}
|
||||
|
||||
|
||||
- (void)toggleEnableAutoReportDecryptionErrors:(UISwitch *)sender
|
||||
{
|
||||
RiotSettings.shared.enableUISIAutoReporting = sender.isOn;
|
||||
}
|
||||
|
||||
#pragma mark - TextField listener
|
||||
|
||||
- (IBAction)textFieldDidChange:(id)sender
|
||||
|
|
Loading…
Reference in a new issue