import Foundation import UserNotifications import SwiftSignalKit import Postbox import TelegramCore import BuildConfig import OpenSSLEncryptionProvider import TelegramUIPreferences import WebPBinding import RLottieBinding import GZip import UIKit import Intents import PersistentStringHash import CallKit import AppLockState import NotificationsPresentationData private let queue = Queue() private var installedSharedLogger = false private func setupSharedLogger(rootPath: String, path: String) { if !installedSharedLogger { installedSharedLogger = true Logger.setSharedLogger(Logger(rootPath: rootPath, basePath: path)) } } private let accountAuxiliaryMethods = AccountAuxiliaryMethods(fetchResource: { account, resource, ranges, _ in return nil }, fetchResourceMediaReferenceHash: { resource in return .single(nil) }, prepareSecretThumbnailData: { _ in return nil }, backgroundUpload: { _, _, _ in return .single(nil) }) private func rootPathForBasePath(_ appGroupPath: String) -> String { return appGroupPath + "/telegram-data" } private let deviceColorSpace: CGColorSpace = { if #available(iOSApplicationExtension 9.3, iOS 9.3, *) { if let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) { return colorSpace } else { return CGColorSpaceCreateDeviceRGB() } } else { return CGColorSpaceCreateDeviceRGB() } }() private func getSharedDevideGraphicsContextSettings() -> DeviceGraphicsContextSettings { struct OpaqueSettings { let rowAlignment: Int let bitsPerPixel: Int let bitsPerComponent: Int let opaqueBitmapInfo: CGBitmapInfo let colorSpace: CGColorSpace init(context: CGContext) { self.rowAlignment = context.bytesPerRow self.bitsPerPixel = context.bitsPerPixel self.bitsPerComponent = context.bitsPerComponent self.opaqueBitmapInfo = context.bitmapInfo if #available(iOS 10.0, *) { if UIScreen.main.traitCollection.displayGamut == .P3 { self.colorSpace = CGColorSpace(name: CGColorSpace.displayP3) ?? context.colorSpace! } else { self.colorSpace = context.colorSpace! } } else { self.colorSpace = context.colorSpace! } assert(self.rowAlignment == 32) assert(self.bitsPerPixel == 32) assert(self.bitsPerComponent == 8) } } struct TransparentSettings { let transparentBitmapInfo: CGBitmapInfo init(context: CGContext) { self.transparentBitmapInfo = context.bitmapInfo } } var opaqueSettings: OpaqueSettings? var transparentSettings: TransparentSettings? if #available(iOS 10.0, *) { let opaqueFormat = UIGraphicsImageRendererFormat() let transparentFormat = UIGraphicsImageRendererFormat() if #available(iOS 12.0, *) { opaqueFormat.preferredRange = .standard transparentFormat.preferredRange = .standard } opaqueFormat.opaque = true transparentFormat.opaque = false let opaqueRenderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), format: opaqueFormat) let _ = opaqueRenderer.image(actions: { context in opaqueSettings = OpaqueSettings(context: context.cgContext) }) let transparentRenderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), format: transparentFormat) let _ = transparentRenderer.image(actions: { context in transparentSettings = TransparentSettings(context: context.cgContext) }) } else { UIGraphicsBeginImageContextWithOptions(CGSize(width: 1.0, height: 1.0), true, 1.0) let refContext = UIGraphicsGetCurrentContext()! opaqueSettings = OpaqueSettings(context: refContext) UIGraphicsEndImageContext() UIGraphicsBeginImageContextWithOptions(CGSize(width: 1.0, height: 1.0), false, 1.0) let refCtxTransparent = UIGraphicsGetCurrentContext()! transparentSettings = TransparentSettings(context: refCtxTransparent) UIGraphicsEndImageContext() } return DeviceGraphicsContextSettings( rowAlignment: opaqueSettings!.rowAlignment, bitsPerPixel: opaqueSettings!.bitsPerPixel, bitsPerComponent: opaqueSettings!.bitsPerComponent, opaqueBitmapInfo: opaqueSettings!.opaqueBitmapInfo, transparentBitmapInfo: transparentSettings!.transparentBitmapInfo, colorSpace: opaqueSettings!.colorSpace ) } public struct DeviceGraphicsContextSettings { public static let shared: DeviceGraphicsContextSettings = getSharedDevideGraphicsContextSettings() public let rowAlignment: Int public let bitsPerPixel: Int public let bitsPerComponent: Int public let opaqueBitmapInfo: CGBitmapInfo public let transparentBitmapInfo: CGBitmapInfo public let colorSpace: CGColorSpace public func bytesPerRow(forWidth width: Int) -> Int { let baseValue = self.bitsPerPixel * width / 8 return (baseValue + 31) & ~0x1F } } private final class DrawingContext { let size: CGSize let scale: CGFloat let scaledSize: CGSize let bytesPerRow: Int private let bitmapInfo: CGBitmapInfo let length: Int let bytes: UnsafeMutableRawPointer private let data: Data private let context: CGContext private var hasGeneratedImage = false func withContext(_ f: (CGContext) -> ()) { let context = self.context context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0) f(context) context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0) } func withFlippedContext(_ f: (CGContext) -> ()) { f(self.context) } init(size: CGSize, scale: CGFloat = 1.0, opaque: Bool = false, clear: Bool = false) { assert(!size.width.isZero && !size.height.isZero) let size: CGSize = CGSize(width: max(1.0, size.width), height: max(1.0, size.height)) let actualScale: CGFloat if scale.isZero { actualScale = 1.0 } else { actualScale = scale } self.size = size self.scale = actualScale self.scaledSize = CGSize(width: size.width * actualScale, height: size.height * actualScale) self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(self.scaledSize.width)) self.length = self.bytesPerRow * Int(self.scaledSize.height) self.bytes = malloc(self.length) self.data = Data(bytesNoCopy: self.bytes, count: self.length, deallocator: .custom({ bytes, _ in free(bytes) })) if opaque { self.bitmapInfo = DeviceGraphicsContextSettings.shared.opaqueBitmapInfo } else { self.bitmapInfo = DeviceGraphicsContextSettings.shared.transparentBitmapInfo } self.context = CGContext( data: self.bytes, width: Int(self.scaledSize.width), height: Int(self.scaledSize.height), bitsPerComponent: 8, bytesPerRow: self.bytesPerRow, space: deviceColorSpace, bitmapInfo: self.bitmapInfo.rawValue, releaseCallback: nil, releaseInfo: nil )! self.context.scaleBy(x: self.scale, y: self.scale) if clear { memset(self.bytes, 0, self.length) } } func generateImage() -> UIImage? { if self.scaledSize.width.isZero || self.scaledSize.height.isZero { return nil } if self.hasGeneratedImage { preconditionFailure() } self.hasGeneratedImage = true guard let dataProvider = CGDataProvider(data: self.data as CFData) else { return nil } if let image = CGImage( width: Int(self.scaledSize.width), height: Int(self.scaledSize.height), bitsPerComponent: self.context.bitsPerComponent, bitsPerPixel: self.context.bitsPerPixel, bytesPerRow: self.context.bytesPerRow, space: DeviceGraphicsContextSettings.shared.colorSpace, bitmapInfo: self.context.bitmapInfo, provider: dataProvider, decode: nil, shouldInterpolate: true, intent: .defaultIntent ) { return UIImage(cgImage: image, scale: self.scale, orientation: .up) } else { return nil } } } private extension CGSize { func fitted(_ size: CGSize) -> CGSize { var fittedSize = self if fittedSize.width > size.width { fittedSize = CGSize(width: size.width, height: floor((fittedSize.height * size.width / max(fittedSize.width, 1.0)))) } if fittedSize.height > size.height { fittedSize = CGSize(width: floor((fittedSize.width * size.height / max(fittedSize.height, 1.0))), height: size.height) } return fittedSize } } private func convertLottieImage(data: Data) -> UIImage? { let decompressedData = TGGUnzipData(data, 512 * 1024) ?? data guard let animation = LottieInstance(data: decompressedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else { return nil } let size = animation.dimensions.fitted(CGSize(width: 200.0, height: 200.0)) let context = DrawingContext(size: size, scale: 1.0, opaque: false, clear: true) animation.renderFrame(with: 0, into: context.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(context.scaledSize.width), height: Int32(context.scaledSize.height), bytesPerRow: Int32(context.bytesPerRow)) return context.generateImage() } private func testAvatarImage(size: CGSize) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, 2.0) let context = UIGraphicsGetCurrentContext()! context.beginPath() context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) context.clip() context.setFillColor(UIColor.red.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } private func avatarRoundImage(size: CGSize, source: UIImage) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, 0.0) let context = UIGraphicsGetCurrentContext() context?.beginPath() context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) context?.clip() source.draw(in: CGRect(origin: CGPoint(), size: size)) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } private extension UIColor { convenience init(rgb: UInt32) { self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0) } } private let gradientColors: [NSArray] = [ [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], [UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor], [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], ] private func avatarViewLettersImage(size: CGSize, peerId: PeerId, letters: [String]) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, 2.0) let context = UIGraphicsGetCurrentContext() context?.beginPath() context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) context?.clip() let colorIndex: Int if peerId.namespace == .max { colorIndex = 0 } else { colorIndex = abs(Int(clamping: peerId.id._internalGetInt64Value())) } let colorsArray = gradientColors[colorIndex % gradientColors.count] var locations: [CGFloat] = [1.0, 0.0] let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! context?.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) context?.setBlendMode(.normal) let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20.0), NSAttributedString.Key.foregroundColor: UIColor.white]) let line = CTLineCreateWithAttributedString(attributedString) let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) let lineOrigin = CGPoint(x: floor(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floor(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0)) context?.translateBy(x: size.width / 2.0, y: size.height / 2.0) context?.scaleBy(x: 1.0, y: -1.0) context?.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) context?.translateBy(x: lineOrigin.x, y: lineOrigin.y) if let context = context { CTLineDraw(line, context) } context?.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } private func avatarImage(path: String?, peerId: PeerId, letters: [String], size: CGSize) -> UIImage { if let path = path, let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image) { return roundImage } else { return avatarViewLettersImage(size: size, peerId: peerId, letters: letters)! } } private func storeTemporaryImage(path: String) -> String { let imagesPath = NSTemporaryDirectory() + "/aps-data" let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: imagesPath), withIntermediateDirectories: true, attributes: nil) let tempPath = imagesPath + "\(path.persistentHashValue)" if FileManager.default.fileExists(atPath: tempPath) { return tempPath } let _ = try? FileManager.default.copyItem(at: URL(fileURLWithPath: path), to: URL(fileURLWithPath: tempPath)) return tempPath } @available(iOS 15.0, *) private func peerAvatar(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer) -> INImage? { if let resource = smallestImageRepresentation(peer.profileImageRepresentations)?.resource, let path = mediaBox.completedResourcePath(resource) { let cachedPath = mediaBox.cachedRepresentationPathForId(resource.id.stringRepresentation, representationId: "intents.png", keepDuration: .shortLived) if let _ = fileSize(cachedPath) { return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) } else { let image = avatarImage(path: path, peerId: peer.id, letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0)) if let data = image.pngData() { let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic) } return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) } } let cachedPath = mediaBox.cachedRepresentationPathForId("lettersAvatar2-\(peer.displayLetters.joined(separator: ","))", representationId: "intents.png", keepDuration: .shortLived) if let _ = fileSize(cachedPath) { return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) } else { let image = avatarImage(path: nil, peerId: peer.id, letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0)) if let data = image.pngData() { let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic) } return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) } } @available(iOSApplicationExtension 10.0, iOS 10.0, *) private struct NotificationContent: CustomStringConvertible { var title: String? var subtitle: String? var body: String? var threadId: String? var sound: String? var badge: Int? var category: String? var userInfo: [AnyHashable: Any] = [:] var attachments: [UNNotificationAttachment] = [] var silent = false var senderPerson: INPerson? var senderImage: INImage? var isLockedMessage: String? init(isLockedMessage: String?) { self.isLockedMessage = isLockedMessage } var description: String { var string = "{" string += " title: \(String(describing: self.title))\n" string += " subtitle: \(String(describing: self.subtitle))\n" string += " body: \(String(describing: self.body)),\n" string += " threadId: \(String(describing: self.threadId)),\n" string += " sound: \(String(describing: self.sound)),\n" string += " badge: \(String(describing: self.badge)),\n" string += " category: \(String(describing: self.category)),\n" string += " userInfo: \(String(describing: self.userInfo)),\n" string += " senderImage: \(self.senderImage != nil ? "non-empty" : "empty"),\n" string += " isLockedMessage: \(String(describing: self.isLockedMessage)),\n" string += " attachments: \(self.attachments),\n" string += "}" return string } mutating func addSenderInfo(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer, topicTitle: String?, contactIdentifier: String?) { if #available(iOS 15.0, *) { let image = peerAvatar(mediaBox: mediaBox, accountPeerId: accountPeerId, peer: peer) self.senderImage = image var displayName: String = peer.debugDisplayTitle if let topicTitle { displayName = "\(topicTitle) (\(displayName))" } if self.silent { displayName = "\(displayName) 🔕" } var personNameComponents = PersonNameComponents() personNameComponents.nickname = displayName self.senderPerson = INPerson( personHandle: INPersonHandle(value: "\(peer.id.toInt64())", type: .unknown), nameComponents: personNameComponents, displayName: displayName, image: image, contactIdentifier: contactIdentifier, customIdentifier: "\(peer.id.toInt64())", isMe: false, suggestionType: .none ) } } func generate() -> UNNotificationContent { var content = UNMutableNotificationContent() //Logger.shared.log("NotificationService", "Generating final content: \(self.description)") if let title = self.title { if self.silent { content.title = "\(title) 🔕" } else { content.title = title } } if let subtitle = self.subtitle { content.subtitle = subtitle } if let body = self.body { content.body = body } if !content.title.isEmpty || !content.subtitle.isEmpty || !content.body.isEmpty { if let isLockedMessage = self.isLockedMessage { content.body = isLockedMessage } } if let threadId = self.threadId { content.threadIdentifier = threadId } if let sound = self.sound { content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: sound)) } if let badge = self.badge { content.badge = badge as NSNumber } if let category = self.category { content.categoryIdentifier = category } if !self.userInfo.isEmpty { content.userInfo = self.userInfo } if !self.attachments.isEmpty { content.attachments = self.attachments } if #available(iOS 15.0, *) { if self.isLockedMessage == nil, let senderPerson = self.senderPerson, let customIdentifier = senderPerson.customIdentifier { let mePerson = INPerson( personHandle: INPersonHandle(value: "0", type: .unknown), nameComponents: nil, displayName: nil, image: nil, contactIdentifier: nil, customIdentifier: nil, isMe: true, suggestionType: .none ) let incomingCommunicationIntent = INSendMessageIntent( recipients: [mePerson], outgoingMessageType: .outgoingMessageText, content: content.body, speakableGroupName: INSpeakableString(spokenPhrase: senderPerson.displayName), conversationIdentifier: "\(customIdentifier)", serviceName: nil, sender: senderPerson, attachments: nil ) if let senderImage = self.senderImage { incomingCommunicationIntent.setImage(senderImage, forParameterNamed: \.sender) } let interaction = INInteraction(intent: incomingCommunicationIntent, response: nil) interaction.direction = .incoming interaction.donate(completion: nil) do { content = try content.updating(from: incomingCommunicationIntent) as! UNMutableNotificationContent } catch let e { print("Exception: \(e)") } } } return content } } private func getCurrentRenderedTotalUnreadCount(accountManager: AccountManager, postbox: Postbox) -> Signal<(Int32, RenderedTotalUnreadCountType), NoError> { let counters = postbox.transaction { transaction -> ChatListTotalUnreadState in return transaction.getTotalUnreadState(groupId: .root) } return combineLatest( accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings]) |> take(1), counters ) |> map { sharedData, totalReadCounters -> (Int32, RenderedTotalUnreadCountType) in let inAppSettings: InAppNotificationSettings if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings]?.get(InAppNotificationSettings.self) { inAppSettings = value } else { inAppSettings = .defaultSettings } let type: RenderedTotalUnreadCountType switch inAppSettings.totalUnreadCountDisplayStyle { case .filtered: type = .filtered } return (totalReadCounters.count(for: inAppSettings.totalUnreadCountDisplayStyle.category, in: inAppSettings.totalUnreadCountDisplayCategory.statsType, with: inAppSettings.totalUnreadCountIncludeTags), type) } } @available(iOSApplicationExtension 10.0, iOS 10.0, *) private final class NotificationServiceHandler { private let queue: Queue private let accountManager: AccountManager private let encryptionParameters: ValueBoxEncryptionParameters private var stateManager: AccountStateManager? private let notificationKeyDisposable = MetaDisposable() private let pollDisposable = MetaDisposable() init?(queue: Queue, updateCurrentContent: @escaping (NotificationContent) -> Void, completed: @escaping () -> Void, payload: [AnyHashable: Any]) { //debug_linker_fail_test() self.queue = queue let episode = String(UInt32.random(in: 0 ..< UInt32.max), radix: 16) guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else { return nil } let baseAppBundleId = String(appBundleIdentifier[..(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false, removeDatabaseOnError: false) let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId) self.encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!) let semaphore = DispatchSemaphore(value: 0) var loggingSettings = LoggingSettings.defaultSettings let _ = (self.accountManager.transaction { transaction -> LoggingSettings in if let value = transaction.getSharedData(SharedDataKeys.loggingSettings)?.get(LoggingSettings.self) { return value } else { return LoggingSettings.defaultSettings } }).start(next: { value in loggingSettings = value semaphore.signal() }) semaphore.wait() Logger.shared.logToFile = loggingSettings.logToFile Logger.shared.logToConsole = loggingSettings.logToConsole Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData let networkArguments = NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), resolvedDeviceName: nil) let isLockedMessage: String? if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { if let notificationsPresentationData = try? Data(contentsOf: URL(fileURLWithPath: notificationsPresentationDataPath(rootPath: rootPath))), let notificationsPresentationDataValue = try? JSONDecoder().decode(NotificationsPresentationData.self, from: notificationsPresentationData) { isLockedMessage = notificationsPresentationDataValue.applicationLockedMessageString } else { isLockedMessage = "You have a new message" } } else { isLockedMessage = nil } let incomingCallMessage: String if let notificationsPresentationData = try? Data(contentsOf: URL(fileURLWithPath: notificationsPresentationDataPath(rootPath: rootPath))), let notificationsPresentationDataValue = try? JSONDecoder().decode(NotificationsPresentationData.self, from: notificationsPresentationData) { incomingCallMessage = notificationsPresentationDataValue.incomingCallString } else { incomingCallMessage = "is calling you" } Logger.shared.log("NotificationService \(episode)", "Begin processing payload") guard var encryptedPayload = payload["p"] as? String else { Logger.shared.log("NotificationService \(episode)", "Invalid payload 1") return nil } encryptedPayload = encryptedPayload.replacingOccurrences(of: "-", with: "+") encryptedPayload = encryptedPayload.replacingOccurrences(of: "_", with: "/") while encryptedPayload.count % 4 != 0 { encryptedPayload.append("=") } guard let payloadData = Data(base64Encoded: encryptedPayload) else { Logger.shared.log("NotificationService \(episode)", "Invalid payload 2") return nil } let _ = (combineLatest(queue: self.queue, self.accountManager.accountRecords(), self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings, ApplicationSpecificSharedDataKeys.voiceCallSettings, SharedDataKeys.loggingSettings]) ) |> take(1) |> deliverOn(self.queue)).start(next: { [weak self] records, sharedData in var recordId: AccountRecordId? var isCurrentAccount: Bool = false let loggingSettings = sharedData.entries[SharedDataKeys.loggingSettings]?.get(LoggingSettings.self) ?? LoggingSettings.defaultSettings Logger.shared.logToFile = loggingSettings.logToFile Logger.shared.logToConsole = loggingSettings.logToConsole if let keyId = notificationPayloadKeyId(data: payloadData) { outer: for listRecord in records.records { for attribute in listRecord.attributes { if case let .backupData(backupData) = attribute { if let notificationEncryptionKeyId = backupData.data?.notificationEncryptionKeyId { if keyId == notificationEncryptionKeyId { recordId = listRecord.id isCurrentAccount = records.currentRecord?.id == listRecord.id break outer } } } } } } let inAppNotificationSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings]?.get(InAppNotificationSettings.self) ?? InAppNotificationSettings.defaultSettings let voiceCallSettings: VoiceCallSettings if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.voiceCallSettings]?.get(VoiceCallSettings.self) { voiceCallSettings = value } else { voiceCallSettings = VoiceCallSettings.defaultSettings } guard let strongSelf = self, let recordId = recordId else { Logger.shared.log("NotificationService \(episode)", "Couldn't find a matching decryption key") let content = NotificationContent(isLockedMessage: nil) updateCurrentContent(content) completed() return } let _ = (standaloneStateManager( accountManager: strongSelf.accountManager, networkArguments: networkArguments, id: recordId, encryptionParameters: strongSelf.encryptionParameters, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods ) |> deliverOn(strongSelf.queue)).start(next: { stateManager in guard let strongSelf = self else { return } guard let stateManager = stateManager else { Logger.shared.log("NotificationService \(episode)", "Didn't receive stateManager") let content = NotificationContent(isLockedMessage: nil) updateCurrentContent(content) completed() return } strongSelf.stateManager = stateManager let settings = stateManager.postbox.transaction { transaction -> NotificationSoundList? in return _internal_cachedNotificationSoundList(transaction: transaction) } strongSelf.notificationKeyDisposable.set((combineLatest(queue: strongSelf.queue, existingMasterNotificationsKey(postbox: stateManager.postbox), settings ) |> deliverOn(strongSelf.queue)).start(next: { notificationsKey, notificationSoundList in guard let strongSelf = self else { let content = NotificationContent(isLockedMessage: nil) updateCurrentContent(content) completed() return } guard let notificationsKey = notificationsKey else { Logger.shared.log("NotificationService \(episode)", "Didn't receive decryption key") let content = NotificationContent(isLockedMessage: nil) updateCurrentContent(content) completed() return } guard let decryptedPayload = decryptedNotificationPayload(key: notificationsKey, data: payloadData) else { Logger.shared.log("NotificationService \(episode)", "Couldn't decrypt payload") let content = NotificationContent(isLockedMessage: nil) updateCurrentContent(content) completed() return } guard let payloadJson = try? JSONSerialization.jsonObject(with: decryptedPayload, options: []) as? [String: Any] else { Logger.shared.log("NotificationService \(episode)", "Couldn't process payload as JSON") let content = NotificationContent(isLockedMessage: nil) updateCurrentContent(content) completed() return } Logger.shared.log("NotificationService \(episode)", "Decrypted payload: \(payloadJson)") var peerId: PeerId? var messageId: MessageId.Id? var mediaAttachment: Media? var downloadNotificationSound: (file: TelegramMediaFile, path: String, fileName: String)? var interactionAuthorId: PeerId? var topicTitle: String? struct CallData { var id: Int64 var accessHash: Int64 var fromId: PeerId var updates: String var accountId: Int64 var peer: EnginePeer? var localContactId: String? } var callData: CallData? if let messageIdString = payloadJson["msg_id"] as? String { messageId = Int32(messageIdString) } if let fromIdString = payloadJson["from_id"] as? String { if let userIdValue = Int64(fromIdString) { peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userIdValue)) } } else if let chatIdString = payloadJson["chat_id"] as? String { if let chatIdValue = Int64(chatIdString) { peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatIdValue)) } } else if let channelIdString = payloadJson["channel_id"] as? String { if let channelIdValue = Int64(channelIdString) { peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelIdValue)) } } else if let encryptionIdString = payloadJson["encryption_id"] as? String { if let encryptionIdValue = Int64(encryptionIdString) { peerId = PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(encryptionIdValue)) } } if let callIdString = payloadJson["call_id"] as? String, let callAccessHashString = payloadJson["call_ah"] as? String, let peerId = peerId, let updates = payloadJson["updates"] as? String { if let callId = Int64(callIdString), let callAccessHash = Int64(callAccessHashString) { var peer: EnginePeer? var updateString = updates updateString = updateString.replacingOccurrences(of: "-", with: "+") updateString = updateString.replacingOccurrences(of: "_", with: "/") while updateString.count % 4 != 0 { updateString.append("=") } if let updateData = Data(base64Encoded: updateString) { if let callUpdate = AccountStateManager.extractIncomingCallUpdate(data: updateData) { peer = callUpdate.peer } } callData = CallData( id: callId, accessHash: callAccessHash, fromId: peerId, updates: updates, accountId: recordId.int64, peer: peer ) } } enum Action { case logout case poll(peerId: PeerId, content: NotificationContent, messageId: MessageId?) case deleteMessage([MessageId]) case readMessage(MessageId) case call(CallData) } var action: Action? if let callData = callData { action = .call(callData) } else if let locKey = payloadJson["loc-key"] as? String { switch locKey { case "SESSION_REVOKE": action = .logout case "MESSAGE_MUTED": if let peerId = peerId { action = .poll(peerId: peerId, content: NotificationContent(isLockedMessage: nil), messageId: nil) } case "MESSAGE_DELETED": if let peerId = peerId { if let messageId = messageId { action = .deleteMessage([MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId)]) } else if let messageIds = payloadJson["messages"] as? String { var messagesDeleted: [MessageId] = [] for messageId in messageIds.split(separator: ",") { if let messageIdValue = Int32(messageId) { messagesDeleted.append(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageIdValue)) } } action = .deleteMessage(messagesDeleted) } } case "READ_HISTORY": if let peerId = peerId { if let messageIdString = payloadJson["max_id"] as? String { if let maxId = Int32(messageIdString) { action = .readMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: maxId)) } } } default: break } } else { if let aps = payloadJson["aps"] as? [String: Any], let peerId = peerId { var content: NotificationContent = NotificationContent(isLockedMessage: isLockedMessage) if let alert = aps["alert"] as? [String: Any] { if let topicTitleValue = payloadJson["topic_title"] as? String { topicTitle = topicTitleValue if let title = alert["title"] as? String { content.title = "\(topicTitleValue) (\(title))" } else { content.title = topicTitleValue } } else { content.title = alert["title"] as? String } content.subtitle = alert["subtitle"] as? String content.body = alert["body"] as? String } else if let alert = aps["alert"] as? String { content.body = alert } else { completed() return } var messageIdValue: MessageId? if let messageId = messageId { content.userInfo["msg_id"] = "\(messageId)" interactionAuthorId = peerId messageIdValue = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId) } if peerId.namespace == Namespaces.Peer.CloudUser { content.userInfo["from_id"] = "\(peerId.id._internalGetInt64Value())" } else if peerId.namespace == Namespaces.Peer.CloudGroup { content.userInfo["chat_id"] = "\(peerId.id._internalGetInt64Value())" } else if peerId.namespace == Namespaces.Peer.CloudChannel { content.userInfo["channel_id"] = "\(peerId.id._internalGetInt64Value())" } content.userInfo["peerId"] = "\(peerId.toInt64())" content.userInfo["accountId"] = "\(recordId.int64)" if let silentString = payloadJson["silent"] as? String { if let silentValue = Int(silentString), silentValue != 0 { content.silent = true } } if var attachmentDataString = payloadJson["attachb64"] as? String { attachmentDataString = attachmentDataString.replacingOccurrences(of: "-", with: "+") attachmentDataString = attachmentDataString.replacingOccurrences(of: "_", with: "/") while attachmentDataString.count % 4 != 0 { attachmentDataString.append("=") } if let attachmentData = Data(base64Encoded: attachmentDataString) { mediaAttachment = _internal_parseMediaAttachment(data: attachmentData) } } if let threadId = aps["thread-id"] as? String { content.threadId = threadId } if let topicIdValue = payloadJson["topic_id"] as? String, let topicId = Int(topicIdValue) { if let threadId = content.threadId { content.threadId = "\(threadId):\(topicId)" } content.userInfo["threadId"] = Int32(clamping: topicId) } if let ringtoneString = aps["ringtone"] as? String, let fileId = Int64(ringtoneString) { content.sound = "0.m4a" if let notificationSoundList = notificationSoundList { for sound in notificationSoundList.sounds { if sound.file.fileId.id == fileId { let containerSoundsPath = appGroupUrl.path + "/Library/Sounds" let soundFileName = "\(fileId).mp3" let soundFilePath = containerSoundsPath + "/\(soundFileName)" if !FileManager.default.fileExists(atPath: soundFilePath) { let _ = try? FileManager.default.createDirectory(atPath: containerSoundsPath, withIntermediateDirectories: true, attributes: nil) if let filePath = stateManager.postbox.mediaBox.completedResourcePath(id: sound.file.resource.id, pathExtension: nil) { let _ = try? FileManager.default.copyItem(atPath: filePath, toPath: soundFilePath) } else { downloadNotificationSound = (sound.file, soundFilePath, soundFileName) } } content.sound = soundFileName break } } } } else if let sound = aps["sound"] as? String { content.sound = sound } if let category = aps["category"] as? String { content.category = category let _ = messageId /*if (peerId != 0 && messageId != 0 && parsedAttachment != nil && attachmentData != nil) { userInfo[@"peerId"] = @(peerId); userInfo[@"messageId.namespace"] = @(0); userInfo[@"messageId.id"] = @(messageId); userInfo[@"media"] = [attachmentData base64EncodedStringWithOptions:0]; if (isExpandableMedia) { if ([categoryString isEqualToString:@"r"]) { _bestAttemptContent.categoryIdentifier = @"withReplyMedia"; } else if ([categoryString isEqualToString:@"m"]) { _bestAttemptContent.categoryIdentifier = @"withMuteMedia"; } } }*/ } /*if (accountInfos.accounts.count > 1) { if (_bestAttemptContent.title.length != 0 && account.peerName.length != 0) { _bestAttemptContent.title = [NSString stringWithFormat:@"%@ → %@", _bestAttemptContent.title, account.peerName]; } }*/ action = .poll(peerId: peerId, content: content, messageId: messageIdValue) updateCurrentContent(content) } } if let action = action { switch action { case let .call(callData): if let stateManager = strongSelf.stateManager { let content = NotificationContent(isLockedMessage: nil) updateCurrentContent(content) let _ = (stateManager.postbox.transaction { transaction -> String? in if let peer = transaction.getPeer(callData.fromId) as? TelegramUser { return peer.phone } else { return nil } }).start(next: { phoneNumber in var voipPayload: [AnyHashable: Any] = [ "call_id": "\(callData.id)", "call_ah": "\(callData.accessHash)", "from_id": "\(callData.fromId.id._internalGetInt64Value())", "updates": callData.updates, "accountId": "\(callData.accountId)" ] if let phoneNumber = phoneNumber { voipPayload["phoneNumber"] = phoneNumber } if #available(iOS 14.5, *), voiceCallSettings.enableSystemIntegration { Logger.shared.log("NotificationService \(episode)", "Will report voip notification") let content = NotificationContent(isLockedMessage: nil) updateCurrentContent(content) CXProvider.reportNewIncomingVoIPPushPayload(voipPayload, completion: { error in Logger.shared.log("NotificationService \(episode)", "Did report voip notification, error: \(String(describing: error))") completed() }) } else { var content = NotificationContent(isLockedMessage: nil) if let peer = callData.peer { content.title = peer.debugDisplayTitle content.body = incomingCallMessage } else { content.body = "Incoming Call" } updateCurrentContent(content) completed() } }) } case .logout: Logger.shared.log("NotificationService \(episode)", "Will logout") let content = NotificationContent(isLockedMessage: nil) updateCurrentContent(content) completed() case let .poll(peerId, initialContent, messageId): Logger.shared.log("NotificationService \(episode)", "Will poll") if let stateManager = strongSelf.stateManager { let pollCompletion: (NotificationContent) -> Void = { content in var content = content queue.async { guard let strongSelf = self, let stateManager = strongSelf.stateManager else { let content = NotificationContent(isLockedMessage: isLockedMessage) updateCurrentContent(content) completed() return } var fetchMediaSignal: Signal = .single(nil) if let mediaAttachment = mediaAttachment { var contentType: MediaResourceUserContentType = .other var fetchResource: TelegramMultipartFetchableResource? if let image = mediaAttachment as? TelegramMediaImage, let representation = largestImageRepresentation(image.representations), let resource = representation.resource as? TelegramMultipartFetchableResource { fetchResource = resource contentType = .image } else if let file = mediaAttachment as? TelegramMediaFile { if file.isSticker { fetchResource = file.resource as? TelegramMultipartFetchableResource contentType = .other } else if file.isVideo { fetchResource = file.previewRepresentations.first?.resource as? TelegramMultipartFetchableResource contentType = .video } else { contentType = .file } } if let resource = fetchResource { if let _ = strongSelf.stateManager?.postbox.mediaBox.completedResourcePath(resource) { } else { let intervals: Signal<[(Range, MediaBoxFetchPriority)], NoError> = .single([(0 ..< Int64.max, MediaBoxFetchPriority.maximum)]) fetchMediaSignal = Signal { subscriber in final class DataValue { var data = Data() var totalSize: Int64? } let collectedData = Atomic(value: DataValue()) return standaloneMultipartFetch( postbox: stateManager.postbox, network: stateManager.network, resource: resource, datacenterId: resource.datacenterId, size: nil, intervals: intervals, parameters: MediaResourceFetchParameters( tag: nil, info: resourceFetchInfo(resource: resource), location: messageId.flatMap { messageId in return MediaResourceStorageLocation( peerId: peerId, messageId: messageId ) }, contentType: contentType, isRandomAccessAllowed: true ), encryptionKey: nil, decryptedSize: nil, continueInBackground: false, useMainConnection: true ).start(next: { result in switch result { case let .dataPart(_, data, _, _): var isCompleted = false let _ = collectedData.modify { current in let current = current current.data.append(data) if let totalSize = current.totalSize, Int64(current.data.count) >= totalSize { isCompleted = true } return current } if isCompleted { subscriber.putNext(collectedData.with({ $0.data })) subscriber.putCompletion() } case let .resourceSizeUpdated(size): var isCompleted = false let _ = collectedData.modify { current in let current = current current.totalSize = size if Int64(current.data.count) >= size { isCompleted = true } return current } if isCompleted { subscriber.putNext(collectedData.with({ $0.data })) subscriber.putCompletion() } default: break } }, error: { _ in subscriber.putNext(nil) subscriber.putCompletion() }, completed: { subscriber.putNext(collectedData.with({ $0.data })) subscriber.putCompletion() }) } } } } var fetchNotificationSoundSignal: Signal = .single(nil) if let (downloadNotificationSound, _, _) = downloadNotificationSound { var fetchResource: TelegramMultipartFetchableResource? fetchResource = downloadNotificationSound.resource as? TelegramMultipartFetchableResource if let resource = fetchResource { if let path = strongSelf.stateManager?.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { fetchNotificationSoundSignal = .single(data) } else { let intervals: Signal<[(Range, MediaBoxFetchPriority)], NoError> = .single([(0 ..< Int64.max, MediaBoxFetchPriority.maximum)]) fetchNotificationSoundSignal = Signal { subscriber in let collectedData = Atomic(value: Data()) return standaloneMultipartFetch( postbox: stateManager.postbox, network: stateManager.network, resource: resource, datacenterId: resource.datacenterId, size: nil, intervals: intervals, parameters: MediaResourceFetchParameters( tag: nil, info: resourceFetchInfo(resource: resource), location: nil, contentType: .other, isRandomAccessAllowed: true ), encryptionKey: nil, decryptedSize: nil, continueInBackground: false, useMainConnection: true ).start(next: { result in switch result { case let .dataPart(_, data, _, _): let _ = collectedData.modify { current in var current = current current.append(data) return current } default: break } }, error: { _ in subscriber.putNext(nil) subscriber.putCompletion() }, completed: { subscriber.putNext(collectedData.with({ $0 })) subscriber.putCompletion() }) } } } } Logger.shared.log("NotificationService \(episode)", "Will fetch media") let _ = (combineLatest(queue: queue, fetchMediaSignal |> timeout(10.0, queue: queue, alternate: .single(nil)), fetchNotificationSoundSignal |> timeout(10.0, queue: queue, alternate: .single(nil)) ) |> deliverOn(queue)).start(next: { mediaData, notificationSoundData in guard let strongSelf = self, let stateManager = strongSelf.stateManager else { completed() return } Logger.shared.log("NotificationService \(episode)", "Did fetch media \(mediaData == nil ? "Non-empty" : "Empty")") if let notificationSoundData = notificationSoundData { Logger.shared.log("NotificationService \(episode)", "Did fetch notificationSoundData") if let (_, filePath, _) = downloadNotificationSound { let _ = try? notificationSoundData.write(to: URL(fileURLWithPath: filePath)) } } Logger.shared.log("NotificationService \(episode)", "Will get unread count") let _ = (getCurrentRenderedTotalUnreadCount( accountManager: strongSelf.accountManager, postbox: stateManager.postbox ) |> deliverOn(strongSelf.queue)).start(next: { value in guard let strongSelf = self, let stateManager = strongSelf.stateManager else { completed() return } if isCurrentAccount { content.badge = Int(value.0) } Logger.shared.log("NotificationService \(episode)", "Unread count: \(value.0), isCurrentAccount: \(isCurrentAccount)") Logger.shared.log("NotificationService \(episode)", "mediaAttachment: \(String(describing: mediaAttachment)), mediaData: \(String(describing: mediaData?.count))") if let image = mediaAttachment as? TelegramMediaImage, let resource = largestImageRepresentation(image.representations)?.resource { if let mediaData = mediaData { stateManager.postbox.mediaBox.storeResourceData(resource.id, data: mediaData, synchronous: true) if let messageId { let _ = addSynchronizeAutosaveItemOperation(postbox: stateManager.postbox, messageId: messageId, mediaId: image.imageId).start() } } if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(resource, pathExtension: "jpg") { if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: storedPath), options: nil) { content.attachments.append(attachment) } } } else if let file = mediaAttachment as? TelegramMediaFile { if file.isStaticSticker { let resource = file.resource if let mediaData = mediaData { stateManager.postbox.mediaBox.storeResourceData(resource.id, data: mediaData, synchronous: true) } if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(resource) { if let data = try? Data(contentsOf: URL(fileURLWithPath: storedPath)), let image = WebP.convert(fromWebP: data) { let tempFile = TempBox.shared.tempFile(fileName: "image.png") let _ = try? image.pngData()?.write(to: URL(fileURLWithPath: tempFile.path)) if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: tempFile.path), options: nil) { content.attachments.append(attachment) } } } } else if file.isAnimatedSticker { let resource = file.resource if let mediaData = mediaData { stateManager.postbox.mediaBox.storeResourceData(resource.id, data: mediaData, synchronous: true) } if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(resource) { if let data = try? Data(contentsOf: URL(fileURLWithPath: storedPath)), let image = convertLottieImage(data: data) { let tempFile = TempBox.shared.tempFile(fileName: "image.png") let _ = try? image.pngData()?.write(to: URL(fileURLWithPath: tempFile.path)) if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: tempFile.path), options: nil) { content.attachments.append(attachment) } } } } else if file.isVideo, let representation = file.previewRepresentations.first { let resource = representation.resource if let mediaData = mediaData { stateManager.postbox.mediaBox.storeResourceData(resource.id, data: mediaData, synchronous: true) } if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(resource, pathExtension: "jpg") { if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: storedPath), options: nil) { content.attachments.append(attachment) } } } } Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)") updateCurrentContent(content) completed() }) }) } } let pollSignal: Signal stateManager.network.shouldKeepConnection.set(.single(true)) if peerId.namespace == Namespaces.Peer.CloudChannel { Logger.shared.log("NotificationService \(episode)", "Will poll channel \(peerId)") pollSignal = standalonePollChannelOnce( accountPeerId: stateManager.accountPeerId, postbox: stateManager.postbox, network: stateManager.network, peerId: peerId, stateManager: stateManager ) } else { Logger.shared.log("NotificationService \(episode)", "Will perform non-specific getDifference") enum ControlError { case restart } let signal = stateManager.standalonePollDifference() |> castError(ControlError.self) |> mapToSignal { result -> Signal in if result { return .complete() } else { return .fail(.restart) } } |> restartIfError pollSignal = signal } let pollWithUpdatedContent: Signal if interactionAuthorId != nil || messageId != nil { pollWithUpdatedContent = stateManager.postbox.transaction { transaction -> NotificationContent in var content = initialContent if let interactionAuthorId = interactionAuthorId { if inAppNotificationSettings.displayNameOnLockscreen, let peer = transaction.getPeer(interactionAuthorId) { var foundLocalId: String? transaction.enumerateDeviceContactImportInfoItems({ _, value in if let value = value as? TelegramDeviceContactImportedData { switch value { case let .imported(data, _, peerId): if peerId == interactionAuthorId { foundLocalId = data.localIdentifiers.first return false } default: break } } return true }) content.addSenderInfo(mediaBox: stateManager.postbox.mediaBox, accountPeerId: stateManager.accountPeerId, peer: peer, topicTitle: topicTitle, contactIdentifier: foundLocalId) } } if let messageId = messageId { if let readState = transaction.getCombinedPeerReadState(messageId.peerId) { for (namespace, state) in readState.states { if namespace == messageId.namespace { switch state { case let .idBased(maxIncomingReadId, _, _, _, _): if maxIncomingReadId >= messageId.id { Logger.shared.log("NotificationService \(episode)", "maxIncomingReadId: \(maxIncomingReadId), messageId: \(messageId.id), skipping") content = NotificationContent(isLockedMessage: nil) } else { Logger.shared.log("NotificationService \(episode)", "maxIncomingReadId: \(maxIncomingReadId), messageId: \(messageId.id), not skipping") } case .indexBased: break } } } } } return content } |> then( pollSignal |> map { _ -> NotificationContent in } ) } else { pollWithUpdatedContent = pollSignal |> map { _ -> NotificationContent in } } var updatedContent = initialContent strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content in updatedContent = content }, completed: { pollCompletion(updatedContent) })) } else { completed() } case let .deleteMessage(ids): Logger.shared.log("NotificationService \(episode)", "Will delete messages \(ids)") let mediaBox = stateManager.postbox.mediaBox let _ = (stateManager.postbox.transaction { transaction -> Void in _internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: ids, deleteMedia: true) } |> deliverOn(strongSelf.queue)).start(completed: { UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in var removeIdentifiers: [String] = [] for notification in notifications { if let peerIdString = notification.request.content.userInfo["peerId"] as? String, let peerIdValue = Int64(peerIdString), let messageIdString = notification.request.content.userInfo["msg_id"] as? String, let messageIdValue = Int32(messageIdString) { for id in ids { if PeerId(peerIdValue) == id.peerId && messageIdValue == id.id { removeIdentifiers.append(notification.request.identifier) } } } } let completeRemoval: () -> Void = { guard let strongSelf = self else { return } let _ = (getCurrentRenderedTotalUnreadCount( accountManager: strongSelf.accountManager, postbox: stateManager.postbox ) |> deliverOn(strongSelf.queue)).start(next: { value in var content = NotificationContent(isLockedMessage: nil) if isCurrentAccount { content.badge = Int(value.0) } Logger.shared.log("NotificationService \(episode)", "Unread count: \(value.0), isCurrentAccount: \(isCurrentAccount)") Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)") updateCurrentContent(content) completed() }) } if !removeIdentifiers.isEmpty { Logger.shared.log("NotificationService \(episode)", "Will try to remove \(removeIdentifiers.count) notifications") UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: removeIdentifiers) queue.after(1.0, { completeRemoval() }) } else { completeRemoval() } }) }) case let .readMessage(id): Logger.shared.log("NotificationService \(episode)", "Will read message \(id)") let _ = (stateManager.postbox.transaction { transaction -> Void in transaction.applyIncomingReadMaxId(id) } |> deliverOn(strongSelf.queue)).start(completed: { UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in var removeIdentifiers: [String] = [] for notification in notifications { if let peerIdString = notification.request.content.userInfo["peerId"] as? String, let peerIdValue = Int64(peerIdString), let messageIdString = notification.request.content.userInfo["msg_id"] as? String, let messageIdValue = Int32(messageIdString) { if PeerId(peerIdValue) == id.peerId && messageIdValue <= id.id { removeIdentifiers.append(notification.request.identifier) } } } let completeRemoval: () -> Void = { guard let strongSelf = self else { return } let _ = (getCurrentRenderedTotalUnreadCount( accountManager: strongSelf.accountManager, postbox: stateManager.postbox ) |> deliverOn(strongSelf.queue)).start(next: { value in var content = NotificationContent(isLockedMessage: nil) if isCurrentAccount { content.badge = Int(value.0) } Logger.shared.log("NotificationService \(episode)", "Unread count: \(value.0), isCurrentAccount: \(isCurrentAccount)") Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)") updateCurrentContent(content) completed() }) } if !removeIdentifiers.isEmpty { Logger.shared.log("NotificationService \(episode)", "Will try to remove \(removeIdentifiers.count) notifications") UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: removeIdentifiers) queue.after(1.0, { completeRemoval() }) } else { completeRemoval() } }) }) } } else { let content = NotificationContent(isLockedMessage: nil) updateCurrentContent(content) completed() } })) }) }) } deinit { self.pollDisposable.dispose() self.stateManager?.network.shouldKeepConnection.set(.single(false)) } } @available(iOSApplicationExtension 10.0, iOS 10.0, *) private final class BoxedNotificationServiceHandler { let value: NotificationServiceHandler? init(value: NotificationServiceHandler?) { self.value = value } } @available(iOSApplicationExtension 10.0, iOS 10.0, *) @objc(NotificationService) final class NotificationService: UNNotificationServiceExtension { private var impl: QueueLocalObject? private var initialContent: UNNotificationContent? private let content = Atomic(value: nil) private var contentHandler: ((UNNotificationContent) -> Void)? override init() { super.init() } override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.initialContent = request.content self.contentHandler = contentHandler self.impl = nil let content = self.content self.impl = QueueLocalObject(queue: queue, generate: { [weak self] in return BoxedNotificationServiceHandler(value: NotificationServiceHandler( queue: queue, updateCurrentContent: { value in let _ = content.swap(value) }, completed: { guard let strongSelf = self else { return } strongSelf.impl = nil if let contentHandler = strongSelf.contentHandler { if let content = content.with({ $0 }) { /*let request = UNNotificationRequest(identifier: UUID().uuidString, content: content.generate(), trigger: .none) UNUserNotificationCenter.current().add(request) contentHandler(UNMutableNotificationContent())*/ contentHandler(content.generate()) } else if let initialContent = strongSelf.initialContent { contentHandler(initialContent) } } }, payload: request.content.userInfo )) }) } override func serviceExtensionTimeWillExpire() { if let contentHandler = self.contentHandler { if let content = self.content.with({ $0 }) { contentHandler(content.generate()) } else if let initialContent = self.initialContent { contentHandler(initialContent) } } } }