import Foundation import UIKit import Display import SwiftSignalKit import Postbox import TelegramCore import MtProtoKit import MessageUI import TelegramPresentationData import TelegramUIPreferences import ItemListUI import PresentationDataUtils import OverlayStatusController import AccountContext import AppBundle import ZipArchive import WebKit import InAppPurchaseManager @objc private final class DebugControllerMailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate { public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { controller.dismiss(animated: true, completion: nil) } } private final class DebugControllerArguments { let sharedContext: SharedAccountContext let context: AccountContext? let mailComposeDelegate: DebugControllerMailComposeDelegate let presentController: (ViewController, ViewControllerPresentationArguments?) -> Void let pushController: (ViewController) -> Void let getRootController: () -> UIViewController? let getNavigationController: () -> NavigationController? init(sharedContext: SharedAccountContext, context: AccountContext?, mailComposeDelegate: DebugControllerMailComposeDelegate, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping (ViewController) -> Void, getRootController: @escaping () -> UIViewController?, getNavigationController: @escaping () -> NavigationController?) { self.sharedContext = sharedContext self.context = context self.mailComposeDelegate = mailComposeDelegate self.presentController = presentController self.pushController = pushController self.getRootController = getRootController self.getNavigationController = getNavigationController } } private enum DebugControllerSection: Int32 { case sticker case logs case logging case experiments case translation case videoExperiments case videoExperiments2 case info } private enum DebugControllerEntry: ItemListNodeEntry { case testStickerImport(PresentationTheme) case sendLogs(PresentationTheme) case sendOneLog(PresentationTheme) case sendShareLogs case sendGroupCallLogs case sendStorageStats case sendNotificationLogs(PresentationTheme) case sendCriticalLogs(PresentationTheme) case sendAllLogs case accounts(PresentationTheme) case logToFile(PresentationTheme, Bool) case logToConsole(PresentationTheme, Bool) case redactSensitiveData(PresentationTheme, Bool) case keepChatNavigationStack(PresentationTheme, Bool) case skipReadHistory(PresentationTheme, Bool) case crashOnSlowQueries(PresentationTheme, Bool) case clearTips(PresentationTheme) case resetNotifications case crash(PresentationTheme) case resetData(PresentationTheme) case resetDatabase(PresentationTheme) case resetDatabaseAndCache(PresentationTheme) case resetHoles(PresentationTheme) case reindexUnread(PresentationTheme) case resetCacheIndex case reindexCache case resetBiometricsData(PresentationTheme) case resetWebViewCache(PresentationTheme) case optimizeDatabase(PresentationTheme) case photoPreview(PresentationTheme, Bool) case knockoutWallpaper(PresentationTheme, Bool) case experimentalCompatibility(Bool) case enableDebugDataDisplay(Bool) case acceleratedStickers(Bool) case inlineForums(Bool) case localTranscription(Bool) case enableReactionOverrides(Bool) case storiesExperiment(Bool) case playlistPlayback(Bool) case enableQuickReactionSwitch(Bool) case voiceConference case preferredVideoCodec(Int, String, String?, Bool) case disableVideoAspectScaling(Bool) case enableNetworkFramework(Bool) case enableNetworkExperiments(Bool) case restorePurchases(PresentationTheme) case logTranslationRecognition(Bool) case resetTranslationStates case hostInfo(PresentationTheme, String) case versionInfo(PresentationTheme) var section: ItemListSectionId { switch self { case .testStickerImport: return DebugControllerSection.sticker.rawValue case .sendLogs, .sendOneLog, .sendShareLogs, .sendGroupCallLogs, .sendStorageStats, .sendNotificationLogs, .sendCriticalLogs, .sendAllLogs: return DebugControllerSection.logs.rawValue case .accounts: return DebugControllerSection.logs.rawValue case .logToFile, .logToConsole, .redactSensitiveData: return DebugControllerSection.logging.rawValue case .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue case .preferredVideoCodec: return DebugControllerSection.videoExperiments.rawValue case .disableVideoAspectScaling, .enableNetworkFramework, .enableNetworkExperiments: return DebugControllerSection.videoExperiments2.rawValue case .hostInfo, .versionInfo: return DebugControllerSection.info.rawValue } } var stableId: Int { switch self { case .testStickerImport: return 0 case .sendLogs: return 1 case .sendOneLog: return 2 case .sendShareLogs: return 3 case .sendGroupCallLogs: return 4 case .sendNotificationLogs: return 5 case .sendCriticalLogs: return 6 case .sendAllLogs: return 7 case .sendStorageStats: return 8 case .accounts: return 9 case .logToFile: return 10 case .logToConsole: return 11 case .redactSensitiveData: return 12 case .keepChatNavigationStack: return 14 case .skipReadHistory: return 15 case .crashOnSlowQueries: return 16 case .clearTips: return 17 case .resetNotifications: return 19 case .crash: return 20 case .resetData: return 21 case .resetDatabase: return 22 case .resetDatabaseAndCache: return 23 case .resetHoles: return 24 case .reindexUnread: return 25 case .resetCacheIndex: return 26 case .reindexCache: return 27 case .resetBiometricsData: return 28 case .resetWebViewCache: return 29 case .optimizeDatabase: return 30 case .photoPreview: return 31 case .knockoutWallpaper: return 32 case .experimentalCompatibility: return 33 case .enableDebugDataDisplay: return 34 case .acceleratedStickers: return 35 case .inlineForums: return 37 case .localTranscription: return 38 case .enableReactionOverrides: return 39 case .restorePurchases: return 40 case .logTranslationRecognition: return 41 case .resetTranslationStates: return 42 case .storiesExperiment: return 43 case .playlistPlayback: return 44 case .enableQuickReactionSwitch: return 45 case .voiceConference: return 46 case let .preferredVideoCodec(index, _, _, _): return 47 + index case .disableVideoAspectScaling: return 100 case .enableNetworkFramework: return 101 case .enableNetworkExperiments: return 102 case .hostInfo: return 103 case .versionInfo: return 104 } } static func <(lhs: DebugControllerEntry, rhs: DebugControllerEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! DebugControllerArguments switch self { case .testStickerImport: return ItemListActionItem(presentationData: presentationData, title: "Simulate Stickers Import", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } if let url = getAppBundle().url(forResource: "importstickers", withExtension: "json"), let data = try? Data(contentsOf: url) { let dataType = "org.telegram.third-party.stickerset" if #available(iOS 10.0, *) { UIPasteboard.general.setItems([[dataType: data]], options: [UIPasteboard.OptionsKey.localOnly: true, UIPasteboard.OptionsKey.expirationDate: NSDate(timeIntervalSinceNow: 60)]) } else { UIPasteboard.general.setData(data, forPasteboardType: dataType) } context.sharedContext.openResolvedUrl(.importStickers, context: context, urlContext: .generic, navigationController: arguments.getNavigationController(), forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { c, a in arguments.presentController(c, a as? ViewControllerPresentationArguments) }, dismissInput: {}, contentContext: nil) } }) case .sendLogs: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Logs (Up to 40 MB)", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectLogs() |> deliverOnMainQueue).start(next: { logs in let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetButtonItem] = [] if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) controller.peerSelected = { [weak controller] peer, _ in let peerId = peer.id if let strongController = controller { strongController.dismiss() let lineFeed = "\n".data(using: .utf8)! var rawLogData: Data = Data() for (name, path) in logs { if !rawLogData.isEmpty { rawLogData.append(lineFeed) rawLogData.append(lineFeed) } rawLogData.append("------ File: \(name) ------\n".data(using: .utf8)!) if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { rawLogData.append(data) } } let tempSource = TempBox.shared.tempFile(fileName: "Log.txt") let tempZip = TempBox.shared.tempFile(fileName: "destination.zip") let _ = try? rawLogData.write(to: URL(fileURLWithPath: tempSource.path)) SSZipArchive.createZipFile(atPath: tempZip.path, withFilesAtPaths: [tempSource.path]) guard let gzippedData = try? Data(contentsOf: URL(fileURLWithPath: tempZip.path)) else { return } TempBox.shared.dispose(tempSource) TempBox.shared.dispose(tempZip) let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } } arguments.pushController(controller) })) } items.append(ActionSheetButtonItem(title: "Via Email", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let composeController = MFMailComposeViewController() composeController.mailComposeDelegate = arguments.mailComposeDelegate composeController.setSubject("Telegram Logs") for (name, path) in logs { if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) { composeController.addAttachmentData(data, mimeType: "application/text", fileName: name) } } arguments.getRootController()?.present(composeController, animated: true, completion: nil) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) arguments.presentController(actionSheet, nil) }) }) case .sendOneLog: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Latest Logs (Up to 4 MB)", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectLogs() |> deliverOnMainQueue).start(next: { logs in let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetButtonItem] = [] if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) controller.peerSelected = { [weak controller] peer, _ in let peerId = peer.id if let strongController = controller { strongController.dismiss() let lineFeed = "\n".data(using: .utf8)! var logData: Data = Data() var latestLogs: [(String, String)] = [] if logs.count < 2 { latestLogs = logs } else { for i in (logs.count - 2) ..< logs.count { latestLogs.append(logs[i]) } } for (name, path) in latestLogs { if !logData.isEmpty { logData.append(lineFeed) logData.append(lineFeed) } logData.append("------ File: \(name) ------\n".data(using: .utf8)!) if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { logData.append(data) } } let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(logData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: logData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(logData.count), attributes: [.FileName(fileName: "Log-iOS-Short.txt")]) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } } arguments.pushController(controller) })) } items.append(ActionSheetButtonItem(title: "Via Email", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let composeController = MFMailComposeViewController() composeController.mailComposeDelegate = arguments.mailComposeDelegate composeController.setSubject("Telegram Logs") for (name, path) in logs { if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) { composeController.addAttachmentData(data, mimeType: "application/text", fileName: name) } } arguments.getRootController()?.present(composeController, animated: true, completion: nil) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) ]) arguments.presentController(actionSheet, nil) }) }) case .sendShareLogs: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Share Logs (Up to 40 MB)", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectLogs(prefix: "/logs/share-logs") |> deliverOnMainQueue).start(next: { logs in let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetButtonItem] = [] if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) controller.peerSelected = { [weak controller] peer, _ in let peerId = peer.id if let strongController = controller { strongController.dismiss() let lineFeed = "\n".data(using: .utf8)! var rawLogData: Data = Data() for (name, path) in logs { if !rawLogData.isEmpty { rawLogData.append(lineFeed) rawLogData.append(lineFeed) } rawLogData.append("------ File: \(name) ------\n".data(using: .utf8)!) if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { rawLogData.append(data) } } let tempSource = TempBox.shared.tempFile(fileName: "Log.txt") let tempZip = TempBox.shared.tempFile(fileName: "destination.zip") let _ = try? rawLogData.write(to: URL(fileURLWithPath: tempSource.path)) SSZipArchive.createZipFile(atPath: tempZip.path, withFilesAtPaths: [tempSource.path]) guard let gzippedData = try? Data(contentsOf: URL(fileURLWithPath: tempZip.path)) else { return } TempBox.shared.dispose(tempSource) TempBox.shared.dispose(tempZip) let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } } arguments.pushController(controller) })) } items.append(ActionSheetButtonItem(title: "Via Email", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let composeController = MFMailComposeViewController() composeController.mailComposeDelegate = arguments.mailComposeDelegate composeController.setSubject("Telegram Logs") for (name, path) in logs { if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) { composeController.addAttachmentData(data, mimeType: "application/text", fileName: name) } } arguments.getRootController()?.present(composeController, animated: true, completion: nil) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) arguments.presentController(actionSheet, nil) }) }) case .sendGroupCallLogs: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Group Call Logs (Up to 40 MB)", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectLogs(basePath: arguments.context!.account.basePath + "/group-calls") |> deliverOnMainQueue).start(next: { logs in let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetButtonItem] = [] if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) controller.peerSelected = { [weak controller] peer, _ in let peerId = peer.id if let strongController = controller { strongController.dismiss() let lineFeed = "\n".data(using: .utf8)! var rawLogData: Data = Data() for (name, path) in logs { if !rawLogData.isEmpty { rawLogData.append(lineFeed) rawLogData.append(lineFeed) } rawLogData.append("------ File: \(name) ------\n".data(using: .utf8)!) if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { rawLogData.append(data) } } let tempSource = TempBox.shared.tempFile(fileName: "Log.txt") let tempZip = TempBox.shared.tempFile(fileName: "destination.zip") let _ = try? rawLogData.write(to: URL(fileURLWithPath: tempSource.path)) SSZipArchive.createZipFile(atPath: tempZip.path, withFilesAtPaths: [tempSource.path]) guard let gzippedData = try? Data(contentsOf: URL(fileURLWithPath: tempZip.path)) else { return } TempBox.shared.dispose(tempSource) TempBox.shared.dispose(tempZip) let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } } arguments.pushController(controller) })) } items.append(ActionSheetButtonItem(title: "Via Email", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let composeController = MFMailComposeViewController() composeController.mailComposeDelegate = arguments.mailComposeDelegate composeController.setSubject("Telegram Logs") for (name, path) in logs { if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) { composeController.addAttachmentData(data, mimeType: "application/text", fileName: name) } } arguments.getRootController()?.present(composeController, animated: true, completion: nil) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) arguments.presentController(actionSheet, nil) }) }) case .sendNotificationLogs: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Notification Logs (Up to 40 MB)", label: "", sectionId: self.section, style: .blocks, action: { let logsPath = arguments.sharedContext.basePath + "/logs/notification-logs" let _ = (Logger(rootPath: logsPath, basePath: logsPath).collectLogs() |> deliverOnMainQueue).start(next: { logs in let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetButtonItem] = [] if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) controller.peerSelected = { [weak controller] peer, _ in let peerId = peer.id if let strongController = controller { strongController.dismiss() let lineFeed = "\n".data(using: .utf8)! var rawLogData: Data = Data() for (name, path) in logs { if !rawLogData.isEmpty { rawLogData.append(lineFeed) rawLogData.append(lineFeed) } rawLogData.append("------ File: \(name) ------\n".data(using: .utf8)!) if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { rawLogData.append(data) } } let tempSource = TempBox.shared.tempFile(fileName: "Log.txt") let tempZip = TempBox.shared.tempFile(fileName: "destination.zip") let _ = try? rawLogData.write(to: URL(fileURLWithPath: tempSource.path)) SSZipArchive.createZipFile(atPath: tempZip.path, withFilesAtPaths: [tempSource.path]) guard let gzippedData = try? Data(contentsOf: URL(fileURLWithPath: tempZip.path)) else { return } TempBox.shared.dispose(tempSource) TempBox.shared.dispose(tempZip) let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } } arguments.pushController(controller) })) } items.append(ActionSheetButtonItem(title: "Via Email", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let composeController = MFMailComposeViewController() composeController.mailComposeDelegate = arguments.mailComposeDelegate composeController.setSubject("Telegram Logs") for (name, path) in logs { if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) { composeController.addAttachmentData(data, mimeType: "application/text", fileName: name) } } arguments.getRootController()?.present(composeController, animated: true, completion: nil) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) arguments.presentController(actionSheet, nil) }) }) case .sendCriticalLogs: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Critical Logs", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectShortLogFiles() |> deliverOnMainQueue).start(next: { logs in let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetButtonItem] = [] if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) controller.peerSelected = { [weak controller] peer, _ in let peerId = peer.id if let strongController = controller { strongController.dismiss() let messages = logs.map { (name, path) -> EnqueueMessage in let id = Int64.random(in: Int64.min ... Int64.max) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) return .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) } let _ = enqueueMessages(account: context.account, peerId: peerId, messages: messages).start() } } arguments.pushController(controller) })) } items.append(ActionSheetButtonItem(title: "Via Email", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let composeController = MFMailComposeViewController() composeController.mailComposeDelegate = arguments.mailComposeDelegate composeController.setSubject("Telegram Logs") for (name, path) in logs { if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) { composeController.addAttachmentData(data, mimeType: "application/text", fileName: name) } } arguments.getRootController()?.present(composeController, animated: true, completion: nil) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) arguments.presentController(actionSheet, nil) }) }) case .sendAllLogs: return ItemListDisclosureItem(presentationData: presentationData, title: "Send All Logs", label: "", sectionId: self.section, style: .blocks, action: { let logTypes: [String] = [ "app-logs", "broadcast-logs", "siri-logs", "widget-logs", "notificationcontent-logs", "notification-logs" ] var logByType: [Signal<(type: String, logs: [(String, String)]), NoError>] = [] for type in logTypes { let logsPath = arguments.sharedContext.basePath + "/logs/\(type)" logByType.append(Logger(rootPath: logsPath, basePath: logsPath).collectLogs() |> map { result -> (type: String, logs: [(String, String)]) in return (type, result) }) } let allLogs = combineLatest(logByType) let _ = (allLogs |> deliverOnMainQueue).start(next: { allLogs in let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetButtonItem] = [] if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) controller.peerSelected = { [weak controller] peer, _ in let peerId = peer.id if let strongController = controller { strongController.dismiss() let lineFeed = "\n".data(using: .utf8)! var tempSources: [TempBoxFile] = [] for (type, logItems) in allLogs { let tempSource = TempBox.shared.tempFile(fileName: "Log-\(type).txt") var rawLogData: Data = Data() for (name, path) in logItems { if !rawLogData.isEmpty { rawLogData.append(lineFeed) rawLogData.append(lineFeed) } rawLogData.append("------ File: \(name) ------\n".data(using: .utf8)!) if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { rawLogData.append(data) } } let _ = try? rawLogData.write(to: URL(fileURLWithPath: tempSource.path)) tempSources.append(tempSource) } let tempZip = TempBox.shared.tempFile(fileName: "destination.zip") SSZipArchive.createZipFile(atPath: tempZip.path, withFilesAtPaths: tempSources.map(\.path)) guard let gzippedData = try? Data(contentsOf: URL(fileURLWithPath: tempZip.path)) else { return } tempSources.forEach(TempBox.shared.dispose) TempBox.shared.dispose(tempZip) let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-All.txt.zip")]) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } } arguments.pushController(controller) })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) arguments.presentController(actionSheet, nil) }) }) case .sendStorageStats: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Storage Stats", label: "", sectionId: self.section, style: .blocks, action: { guard let context = arguments.context, context.sharedContext.applicationBindings.isMainApp else { return } let allStats: Signal = Signal { subscriber in DispatchQueue.global().async { let log = collectRawStorageUsageReport(containerPath: context.sharedContext.applicationBindings.containerPath) subscriber.putNext(log.data(using: .utf8) ?? Data()) } return EmptyDisposable } let _ = (allStats |> deliverOnMainQueue).start(next: { allStatsData in let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetButtonItem] = [] if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) controller.peerSelected = { [weak controller] peer, _ in let peerId = peer.id if let strongController = controller { strongController.dismiss() let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(allStatsData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: allStatsData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(allStatsData.count), attributes: [.FileName(fileName: "StorageReport.txt")]) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } } arguments.pushController(controller) })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) arguments.presentController(actionSheet, nil) }) }) case .accounts: return ItemListDisclosureItem(presentationData: presentationData, title: "Accounts", label: "", sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } arguments.pushController(debugAccountsController(context: context, accountManager: arguments.sharedContext.accountManager)) }) case let .logToFile(_, value): return ItemListSwitchItem(presentationData: presentationData, title: "Log to File", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateLoggingSettings(accountManager: arguments.sharedContext.accountManager, { $0.withUpdatedLogToFile(value) }).start() }) case let .logToConsole(_, value): return ItemListSwitchItem(presentationData: presentationData, title: "Log to Console", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateLoggingSettings(accountManager: arguments.sharedContext.accountManager, { $0.withUpdatedLogToConsole(value) }).start() }) case let .redactSensitiveData(_, value): return ItemListSwitchItem(presentationData: presentationData, title: "Remove Sensitive Data", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateLoggingSettings(accountManager: arguments.sharedContext.accountManager, { $0.withUpdatedRedactSensitiveData(value) }).start() }) case let .keepChatNavigationStack(_, value): return ItemListSwitchItem(presentationData: presentationData, title: "Keep Chat Stack", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in var settings = settings settings.keepChatNavigationStack = value return settings }).start() }) case let .skipReadHistory(_, value): return ItemListSwitchItem(presentationData: presentationData, title: "Skip read history", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in var settings = settings settings.skipReadHistory = value return settings }).start() }) case let .crashOnSlowQueries(_, value): return ItemListSwitchItem(presentationData: presentationData, title: "Crash when slow", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in var settings = settings settings.crashOnLongQueries = value return settings }).start() }) case .clearTips: return ItemListActionItem(presentationData: presentationData, title: "Clear Tips", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { let _ = (arguments.sharedContext.accountManager.transaction { transaction -> Void in transaction.clearNotices() }).start() if let context = arguments.context { let _ = context.engine.itemCache.clear(collectionIds: [ Namespaces.CachedItemCollection.cachedPollResults, Namespaces.CachedItemCollection.cachedStickerPacks ]).start() let _ = context.engine.peers.unmarkChatListFeaturedFiltersAsSeen() } }) case let .logTranslationRecognition(value): return ItemListSwitchItem(presentationData: presentationData, title: "Log Language Recognition", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in var settings = settings settings.logLanguageRecognition = value return settings }).start() }) case .resetTranslationStates: return ItemListActionItem(presentationData: presentationData, title: "Reset Translation States", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { if let context = arguments.context { let _ = context.engine.itemCache.clear(collectionIds: [ ApplicationSpecificItemCacheCollectionId.translationState ]).start() } }) case .resetNotifications: return ItemListActionItem(presentationData: presentationData, title: "Reset Notifications", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { UIApplication.shared.unregisterForRemoteNotifications() if let context = arguments.context { let controller = textAlertController(context: context, title: nil, text: "Now restart the app", actions: [TextAlertAction(type: .genericAction, title: "OK", action: {})]) arguments.presentController(controller, nil) } }) case .crash: return ItemListActionItem(presentationData: presentationData, title: "Crash", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { preconditionFailure() }) case .resetData: return ItemListActionItem(presentationData: presentationData, title: "Reset Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: "All data will be lost."), ActionSheetButtonItem(title: "Reset Data", color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let databasePath = arguments.sharedContext.accountManager.basePath + "/db" let _ = try? FileManager.default.removeItem(atPath: databasePath) preconditionFailure() }), ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) arguments.presentController(actionSheet, nil) }) case .resetDatabase: return ItemListActionItem(presentationData: presentationData, title: "Clear Database", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: "All secret chats will be lost."), ActionSheetButtonItem(title: "Clear Database", color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let databasePath = context.account.basePath + "/postbox/db" let _ = try? FileManager.default.removeItem(atPath: databasePath) exit(0) }), ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) arguments.presentController(actionSheet, nil) }) case .resetDatabaseAndCache: return ItemListActionItem(presentationData: presentationData, title: "Clear Database and Cache", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: "All secret chats will be lost."), ActionSheetButtonItem(title: "Clear Database", color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let databasePath = context.account.basePath + "/postbox" let _ = try? FileManager.default.removeItem(atPath: databasePath) exit(0) }), ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) arguments.presentController(actionSheet, nil) }) case .resetHoles: return ItemListActionItem(presentationData: presentationData, title: "Reset Holes", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) arguments.presentController(controller, nil) let _ = (context.engine.messages.debugAddHoles() |> deliverOnMainQueue).start(completed: { controller.dismiss() }) }) case .reindexUnread: return ItemListActionItem(presentationData: presentationData, title: "Reindex Unread Counters", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) arguments.presentController(controller, nil) let _ = (context.engine.messages.debugReindexUnreadCounters() |> deliverOnMainQueue).start(completed: { controller.dismiss() }) }) case .resetCacheIndex: return ItemListActionItem(presentationData: presentationData, title: "Reset Cache Index [!]", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } context.account.postbox.mediaBox.storageBox.reset() }) case .reindexCache: return ItemListActionItem(presentationData: presentationData, title: "Reindex Cache", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } var signal = context.engine.resources.reindexCacheInBackground(lowImpact: false) var cancelImpl: (() -> Void)? let presentationData = context.sharedContext.currentPresentationData.with { $0 } let progressSignal = Signal { subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) arguments.presentController(controller, nil) return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() } } } |> runOn(Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() let reindexDisposable = MetaDisposable() signal = signal |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } cancelImpl = { reindexDisposable.set(nil) } reindexDisposable.set((signal |> deliverOnMainQueue).start(completed: { })) }) case .resetBiometricsData: return ItemListActionItem(presentationData: presentationData, title: "Reset Biometrics Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { let _ = updatePresentationPasscodeSettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in return settings.withUpdatedBiometricsDomainState(nil).withUpdatedShareBiometricsDomainState(nil) }).start() }) case .resetWebViewCache: return ItemListActionItem(presentationData: presentationData, title: "Clear Web View Cache", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{ }) }) case .optimizeDatabase: return ItemListActionItem(presentationData: presentationData, title: "Optimize Database", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) arguments.presentController(controller, nil) let _ = (context.account.postbox.optimizeStorage() |> deliverOnMainQueue).start(completed: { controller.dismiss() let controller = OverlayStatusController(theme: presentationData.theme, type: .success) arguments.presentController(controller, nil) }) }) case let .photoPreview(_, value): return ItemListSwitchItem(presentationData: presentationData, title: "Media Preview (Updated)", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.chatListPhotos = value return PreferencesEntry(settings) }) }).start() }) case let .knockoutWallpaper(_, value): return ItemListSwitchItem(presentationData: presentationData, title: "Knockout Wallpaper", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.knockoutWallpaper = value return PreferencesEntry(settings) }) }).start() }) case let .experimentalCompatibility(value): return ItemListSwitchItem(presentationData: presentationData, title: "Experimental Compatibility", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.experimentalCompatibility = value return PreferencesEntry(settings) }) }).start() }) case let .enableDebugDataDisplay(value): return ItemListSwitchItem(presentationData: presentationData, title: "Debug Data Display", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.enableDebugDataDisplay = value return PreferencesEntry(settings) }) }).start() }) case let .acceleratedStickers(value): return ItemListSwitchItem(presentationData: presentationData, title: "Accelerated Stickers", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.acceleratedStickers = value return PreferencesEntry(settings) }) }).start() }) case let .inlineForums(value): return ItemListSwitchItem(presentationData: presentationData, title: "Inline Forums", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.inlineForums = value return PreferencesEntry(settings) }) }).start() }) case let .localTranscription(value): return ItemListSwitchItem(presentationData: presentationData, title: "Local Transcription", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.localTranscription = value return PreferencesEntry(settings) }) }).start() }) case let .enableReactionOverrides(value): return ItemListSwitchItem(presentationData: presentationData, title: "Effect Overrides", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.enableReactionOverrides = value if !value { settings.accountReactionEffectOverrides.removeAll() settings.accountStickerEffectOverrides.removeAll() } return PreferencesEntry(settings) }) }).start() }) case let .storiesExperiment(value): return ItemListSwitchItem(presentationData: presentationData, title: "Gallery X", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.storiesExperiment = value return PreferencesEntry(settings) }) }).start() }) case let .playlistPlayback(value): return ItemListSwitchItem(presentationData: presentationData, title: "Playlist Playback", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.playlistPlayback = value return PreferencesEntry(settings) }) }).start() }) case let .enableQuickReactionSwitch(value): return ItemListSwitchItem(presentationData: presentationData, title: "Enable Quick Reaction", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.disableQuickReaction = !value return PreferencesEntry(settings) }) }).start() }) case .voiceConference: return ItemListDisclosureItem(presentationData: presentationData, title: "Voice Conference (Test)", label: "", sectionId: self.section, style: .blocks, action: { guard let _ = arguments.context else { return } }) case let .preferredVideoCodec(_, title, value, isSelected): return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .right, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: { let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.preferredVideoCodec = value return PreferencesEntry(settings) }) }).start() }) case let .disableVideoAspectScaling(value): return ItemListSwitchItem(presentationData: presentationData, title: "Video Cropping Optimization", value: !value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings settings.disableVideoAspectScaling = !value return PreferencesEntry(settings) }) }).start() }) case let .enableNetworkFramework(value): return ItemListSwitchItem(presentationData: presentationData, title: "Network X [Restart App]", value: value, sectionId: self.section, style: .blocks, updated: { value in if let context = arguments.context { let _ = updateNetworkSettingsInteractively(postbox: context.account.postbox, network: context.account.network, { settings in var settings = settings settings.useNetworkFramework = value return settings }).start() } }) case let .enableNetworkExperiments(value): return ItemListSwitchItem(presentationData: presentationData, title: "Download X [Restart App]", value: value, sectionId: self.section, style: .blocks, updated: { value in if let context = arguments.context { let _ = updateNetworkSettingsInteractively(postbox: context.account.postbox, network: context.account.network, { settings in var settings = settings settings.useExperimentalDownload = value return settings }).start() } }) case .restorePurchases: return ItemListActionItem(presentationData: presentationData, title: "Restore Purchases", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.context?.inAppPurchaseManager?.restorePurchases(completion: { state in let text: String switch state { case .succeed: text = "Done" case .failed: text = "Failed" } if let context = arguments.context { let controller = textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: "OK", action: {})]) arguments.presentController(controller, nil) } }) }) case let .hostInfo(_, string): return ItemListTextItem(presentationData: presentationData, text: .plain(string), sectionId: self.section) case .versionInfo: let bundle = Bundle.main let bundleId = bundle.bundleIdentifier ?? "" let bundleVersion = bundle.infoDictionary?["CFBundleShortVersionString"] ?? "" let bundleBuild = bundle.infoDictionary?[kCFBundleVersionKey as String] ?? "" return ItemListTextItem(presentationData: presentationData, text: .plain("\(bundleId)\n\(bundleVersion) (\(bundleBuild))"), sectionId: self.section) } } } private func debugControllerEntries(sharedContext: SharedAccountContext, presentationData: PresentationData, loggingSettings: LoggingSettings, mediaInputSettings: MediaInputSettings, experimentalSettings: ExperimentalUISettings, networkSettings: NetworkSettings?, hasLegacyAppData: Bool, useBetaFeatures: Bool) -> [DebugControllerEntry] { var entries: [DebugControllerEntry] = [] let isMainApp = sharedContext.applicationBindings.isMainApp // entries.append(.testStickerImport(presentationData.theme)) entries.append(.sendLogs(presentationData.theme)) //entries.append(.sendOneLog(presentationData.theme)) entries.append(.sendShareLogs) entries.append(.sendGroupCallLogs) entries.append(.sendNotificationLogs(presentationData.theme)) entries.append(.sendCriticalLogs(presentationData.theme)) entries.append(.sendAllLogs) entries.append(.sendStorageStats) if isMainApp { entries.append(.accounts(presentationData.theme)) } entries.append(.logToFile(presentationData.theme, loggingSettings.logToFile)) entries.append(.logToConsole(presentationData.theme, loggingSettings.logToConsole)) entries.append(.redactSensitiveData(presentationData.theme, loggingSettings.redactSensitiveData)) if isMainApp { entries.append(.keepChatNavigationStack(presentationData.theme, experimentalSettings.keepChatNavigationStack)) #if DEBUG entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory)) #endif } entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries)) if isMainApp { entries.append(.clearTips(presentationData.theme)) entries.append(.resetNotifications) } entries.append(.crash(presentationData.theme)) entries.append(.resetData(presentationData.theme)) entries.append(.resetDatabase(presentationData.theme)) entries.append(.resetDatabaseAndCache(presentationData.theme)) entries.append(.resetHoles(presentationData.theme)) if isMainApp { entries.append(.reindexUnread(presentationData.theme)) entries.append(.resetCacheIndex) entries.append(.reindexCache) entries.append(.resetWebViewCache(presentationData.theme)) } entries.append(.optimizeDatabase(presentationData.theme)) if isMainApp { entries.append(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper)) entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility)) entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay)) entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers)) entries.append(.inlineForums(experimentalSettings.inlineForums)) entries.append(.localTranscription(experimentalSettings.localTranscription)) if case .internal = sharedContext.applicationBindings.appBuildType { entries.append(.enableReactionOverrides(experimentalSettings.enableReactionOverrides)) } entries.append(.restorePurchases(presentationData.theme)) entries.append(.logTranslationRecognition(experimentalSettings.logLanguageRecognition)) entries.append(.resetTranslationStates) if case .internal = sharedContext.applicationBindings.appBuildType { entries.append(.storiesExperiment(experimentalSettings.storiesExperiment)) } entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction)) } let codecs: [(String, String?)] = [ ("No Preference", nil), ("H265", "H265"), ("H264", "H264"), ("VP8", "VP8"), ("VP9", "VP9") ] for i in 0 ..< codecs.count { entries.append(.preferredVideoCodec(i, codecs[i].0, codecs[i].1, experimentalSettings.preferredVideoCodec == codecs[i].1)) } if isMainApp { entries.append(.disableVideoAspectScaling(experimentalSettings.disableVideoAspectScaling)) entries.append(.enableNetworkFramework(networkSettings?.useNetworkFramework ?? useBetaFeatures)) entries.append(.enableNetworkExperiments(networkSettings?.useExperimentalDownload ?? false)) } if let backupHostOverride = networkSettings?.backupHostOverride { entries.append(.hostInfo(presentationData.theme, "Host: \(backupHostOverride)")) } entries.append(.versionInfo(presentationData.theme)) return entries } public func debugController(sharedContext: SharedAccountContext, context: AccountContext?, modal: Bool = false) -> ViewController { var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? var getRootControllerImpl: (() -> UIViewController?)? var getNavigationControllerImpl: (() -> NavigationController?)? let arguments = DebugControllerArguments(sharedContext: sharedContext, context: context, mailComposeDelegate: DebugControllerMailComposeDelegate(), presentController: { controller, arguments in presentControllerImpl?(controller, arguments) }, pushController: { controller in pushControllerImpl?(controller) }, getRootController: { return getRootControllerImpl?() }, getNavigationController: { return getNavigationControllerImpl?() }) let appGroupName = "group.\(Bundle.main.bundleIdentifier!)" let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) var hasLegacyAppData = false if let appGroupUrl = maybeAppGroupUrl { let statusPath = appGroupUrl.path + "/Documents/importcompleted" hasLegacyAppData = FileManager.default.fileExists(atPath: statusPath) } let preferencesSignal: Signal if let context = context { preferencesSignal = context.account.postbox.preferencesView(keys: [PreferencesKeys.networkSettings]) |> map(Optional.init) } else { preferencesSignal = .single(nil) } let signal = combineLatest(sharedContext.presentationData, sharedContext.accountManager.sharedData(keys: Set([SharedDataKeys.loggingSettings, ApplicationSpecificSharedDataKeys.mediaInputSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings])), preferencesSignal) |> map { presentationData, sharedData, preferences -> (ItemListControllerState, (ItemListNodeState, Any)) in let loggingSettings: LoggingSettings if let value = sharedData.entries[SharedDataKeys.loggingSettings]?.get(LoggingSettings.self) { loggingSettings = value } else { loggingSettings = LoggingSettings.defaultSettings } let mediaInputSettings: MediaInputSettings if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaInputSettings]?.get(MediaInputSettings.self) { mediaInputSettings = value } else { mediaInputSettings = MediaInputSettings.defaultSettings } let experimentalSettings: ExperimentalUISettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings let networkSettings: NetworkSettings? = preferences?.values[PreferencesKeys.networkSettings]?.get(NetworkSettings.self) var leftNavigationButton: ItemListNavigationButton? if modal { leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) } var useBetaFeatures: Bool = false if let context { useBetaFeatures = context.account.network.useBetaFeatures } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Debug"), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: debugControllerEntries(sharedContext: sharedContext, presentationData: presentationData, loggingSettings: loggingSettings, mediaInputSettings: mediaInputSettings, experimentalSettings: experimentalSettings, networkSettings: networkSettings, hasLegacyAppData: hasLegacyAppData, useBetaFeatures: useBetaFeatures), style: .blocks) return (controllerState, (listState, arguments)) } let controller = ItemListController(sharedContext: sharedContext, state: signal) presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } dismissImpl = { [weak controller] in controller?.dismiss() } getRootControllerImpl = { [weak controller] in return controller?.view.window?.rootViewController } getNavigationControllerImpl = { [weak controller] in return controller?.navigationController as? NavigationController } return controller } public func triggerDebugSendLogsUI(context: AccountContext, additionalInfo: String = "", pushController: @escaping (ViewController) -> Void) { let _ = (Logger.shared.collectLogs() |> deliverOnMainQueue).start(next: { logs in let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) controller.peerSelected = { [weak controller] peer, _ in let peerId = peer.id if let strongController = controller { strongController.dismiss() let lineFeed = "\n".data(using: .utf8)! var rawLogData: Data = Data() for (name, path) in logs { if !rawLogData.isEmpty { rawLogData.append(lineFeed) rawLogData.append(lineFeed) } rawLogData.append("------ File: \(name) ------\n".data(using: .utf8)!) if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { rawLogData.append(data) } } if !additionalInfo.isEmpty { rawLogData.append("------ Additional Info ------\n".data(using: .utf8)!) rawLogData.append("\(additionalInfo)".data(using: .utf8)!) } let tempSource = TempBox.shared.tempFile(fileName: "Log.txt") let tempZip = TempBox.shared.tempFile(fileName: "destination.zip") let _ = try? rawLogData.write(to: URL(fileURLWithPath: tempSource.path)) SSZipArchive.createZipFile(atPath: tempZip.path, withFilesAtPaths: [tempSource.path]) guard let gzippedData = try? Data(contentsOf: URL(fileURLWithPath: tempZip.path)) else { return } TempBox.shared.dispose(tempSource) TempBox.shared.dispose(tempZip) let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } } pushController(controller) }) }