mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
515 lines
23 KiB
Swift
515 lines
23 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import UserNotifications
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import SyncCore
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import TelegramCallsUI
|
|
import AccountContext
|
|
|
|
private final class PollStateContext {
|
|
let subscribers = Bag<(Bool) -> Void>()
|
|
var disposable: Disposable?
|
|
|
|
deinit {
|
|
self.disposable?.dispose()
|
|
}
|
|
|
|
var isEmpty: Bool {
|
|
return self.disposable == nil && self.subscribers.isEmpty
|
|
}
|
|
}
|
|
|
|
private final class NotificationInfo {
|
|
let dict: [AnyHashable: Any]
|
|
|
|
init(dict: [AnyHashable: Any]) {
|
|
self.dict = dict
|
|
}
|
|
}
|
|
|
|
public final class SharedNotificationManager {
|
|
private let episodeId: UInt32
|
|
private let application: UIApplication
|
|
|
|
private let clearNotificationsManager: ClearNotificationsManager?
|
|
private let pollLiveLocationOnce: (AccountRecordId) -> Void
|
|
|
|
private var inForeground: Bool = false
|
|
private var inForegroundDisposable: Disposable?
|
|
|
|
private var accountManager: AccountManager?
|
|
private var accountsAndKeys: [(Account, Bool, MasterNotificationKey)]?
|
|
private var accountsAndKeysDisposable: Disposable?
|
|
|
|
private var notifications: [NotificationInfo] = []
|
|
|
|
private var pollStateContexts: [AccountRecordId: PollStateContext] = [:]
|
|
|
|
init(episodeId: UInt32, application: UIApplication, clearNotificationsManager: ClearNotificationsManager?, inForeground: Signal<Bool, NoError>, accounts: Signal<[(Account, Bool)], NoError>, pollLiveLocationOnce: @escaping (AccountRecordId) -> Void) {
|
|
assert(Queue.mainQueue().isCurrent())
|
|
|
|
self.episodeId = episodeId
|
|
self.application = application
|
|
self.clearNotificationsManager = clearNotificationsManager
|
|
self.pollLiveLocationOnce = pollLiveLocationOnce
|
|
|
|
self.inForegroundDisposable = (inForeground
|
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.inForeground = value
|
|
})
|
|
|
|
self.accountsAndKeysDisposable = (accounts
|
|
|> mapToSignal { accounts -> Signal<[(Account, Bool, MasterNotificationKey)], NoError> in
|
|
let signals = accounts.map { account, isCurrent -> Signal<(Account, Bool, MasterNotificationKey), NoError> in
|
|
return masterNotificationsKey(account: account, ignoreDisabled: true)
|
|
|> map { key -> (Account, Bool, MasterNotificationKey) in
|
|
return (account, isCurrent, key)
|
|
}
|
|
}
|
|
return combineLatest(signals)
|
|
}
|
|
|> deliverOnMainQueue).start(next: { [weak self] accountsAndKeys in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let shouldProcess = strongSelf.accountsAndKeys == nil
|
|
strongSelf.accountsAndKeys = accountsAndKeys
|
|
if shouldProcess {
|
|
strongSelf.process()
|
|
}
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.inForegroundDisposable?.dispose()
|
|
self.accountsAndKeysDisposable?.dispose()
|
|
}
|
|
|
|
func isPollingState(accountId: AccountRecordId) -> Signal<Bool, NoError> {
|
|
return Signal { subscriber in
|
|
let context: PollStateContext
|
|
if let current = self.pollStateContexts[accountId] {
|
|
context = current
|
|
} else {
|
|
context = PollStateContext()
|
|
self.pollStateContexts[accountId] = context
|
|
}
|
|
subscriber.putNext(context.disposable != nil)
|
|
let index = context.subscribers.add({ value in
|
|
subscriber.putNext(value)
|
|
})
|
|
|
|
return ActionDisposable { [weak context] in
|
|
Queue.mainQueue().async {
|
|
if let current = self.pollStateContexts[accountId], current === context {
|
|
current.subscribers.remove(index)
|
|
if current.isEmpty {
|
|
self.pollStateContexts.removeValue(forKey: accountId)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func beginPollingState(account: Account) {
|
|
let accountId = account.id
|
|
let context: PollStateContext
|
|
if let current = self.pollStateContexts[accountId] {
|
|
context = current
|
|
} else {
|
|
context = PollStateContext()
|
|
self.pollStateContexts[accountId] = context
|
|
}
|
|
let previousDisposable = context.disposable
|
|
context.disposable = (account.stateManager.pollStateUpdateCompletion()
|
|
|> mapToSignal { messageIds -> Signal<[MessageId], NoError> in
|
|
return .single(messageIds)
|
|
|> delay(1.0, queue: Queue.mainQueue())
|
|
}
|
|
|> deliverOnMainQueue).start(next: { [weak self, weak context] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if let current = strongSelf.pollStateContexts[accountId], current === context {
|
|
if let disposable = current.disposable {
|
|
disposable.dispose()
|
|
current.disposable = nil
|
|
for f in current.subscribers.copyItems() {
|
|
f(false)
|
|
}
|
|
}
|
|
if current.isEmpty {
|
|
strongSelf.pollStateContexts.removeValue(forKey: accountId)
|
|
}
|
|
}
|
|
})
|
|
previousDisposable?.dispose()
|
|
if previousDisposable == nil {
|
|
for f in context.subscribers.copyItems() {
|
|
f(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
func addNotification(_ dict: [AnyHashable: Any]) {
|
|
self.notifications.append(NotificationInfo(dict: dict))
|
|
|
|
if self.accountsAndKeys != nil {
|
|
self.process()
|
|
}
|
|
}
|
|
|
|
private func process() {
|
|
guard let accountsAndKeys = self.accountsAndKeys else {
|
|
return
|
|
}
|
|
var decryptedNotifications: [(Account, Bool, [AnyHashable: Any])] = []
|
|
for notification in self.notifications {
|
|
if var encryptedPayload = notification.dict["p"] as? String {
|
|
encryptedPayload = encryptedPayload.replacingOccurrences(of: "-", with: "+")
|
|
encryptedPayload = encryptedPayload.replacingOccurrences(of: "_", with: "/")
|
|
while encryptedPayload.count % 4 != 0 {
|
|
encryptedPayload.append("=")
|
|
}
|
|
if let data = Data(base64Encoded: encryptedPayload) {
|
|
inner: for (account, isCurrent, key) in accountsAndKeys {
|
|
if let decryptedData = decryptedNotificationPayload(key: key, data: data) {
|
|
if let decryptedDict = (try? JSONSerialization.jsonObject(with: decryptedData, options: [])) as? [AnyHashable: Any] {
|
|
decryptedNotifications.append((account, isCurrent, decryptedDict))
|
|
}
|
|
break inner
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.notifications.removeAll()
|
|
|
|
for (account, isCurrent, payload) in decryptedNotifications {
|
|
var redactedPayload = payload
|
|
if var aps = redactedPayload["aps"] as? [AnyHashable: Any] {
|
|
if Logger.shared.redactSensitiveData {
|
|
if aps["alert"] != nil {
|
|
aps["alert"] = "[[redacted]]"
|
|
}
|
|
if aps["body"] != nil {
|
|
aps["body"] = "[[redacted]]"
|
|
}
|
|
}
|
|
redactedPayload["aps"] = aps
|
|
}
|
|
Logger.shared.log("Apns \(self.episodeId)", "\(redactedPayload)")
|
|
|
|
let aps = payload["aps"] as? [AnyHashable: Any]
|
|
|
|
var readMessageId: MessageId?
|
|
var isForcedLogOut = false
|
|
var isCall = false
|
|
var isAnnouncement = false
|
|
var isLocationPolling = false
|
|
var notificationRequestId: NotificationManagedNotificationRequestId?
|
|
var shouldPollState = false
|
|
var title: String = ""
|
|
var body: String?
|
|
var apnsSound: String?
|
|
var configurationUpdate: (Int32, String, Int32, Data?)?
|
|
var messagesDeleted: [MessageId] = []
|
|
if let aps = aps, let alert = aps["alert"] as? String {
|
|
if let range = alert.range(of: ": ") {
|
|
title = String(alert[..<range.lowerBound])
|
|
body = String(alert[range.upperBound...])
|
|
} else {
|
|
body = alert
|
|
}
|
|
} else if let aps = aps, let alert = aps["alert"] as? [AnyHashable: AnyObject] {
|
|
if let alertBody = alert["body"] as? String {
|
|
body = alertBody
|
|
if let alertTitle = alert["title"] as? String {
|
|
title = alertTitle
|
|
}
|
|
}
|
|
}
|
|
if let locKey = payload["loc-key"] as? String {
|
|
if locKey == "SESSION_REVOKE" {
|
|
isForcedLogOut = true
|
|
} else if locKey == "PHONE_CALL_REQUEST" {
|
|
isCall = true
|
|
} else if locKey == "GEO_LIVE_PENDING" {
|
|
isLocationPolling = true
|
|
} else if locKey == "MESSAGE_MUTED" {
|
|
shouldPollState = true
|
|
} else if locKey == "MESSAGE_DELETED" {
|
|
var peerId: PeerId?
|
|
if let fromId = payload["from_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue)))
|
|
} else if let fromId = payload["chat_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue)))
|
|
} else if let fromId = payload["channel_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue)))
|
|
}
|
|
if let peerId = peerId {
|
|
if let messageIds = payload["messages"] as? String {
|
|
for messageId in messageIds.split(separator: ",") {
|
|
if let messageIdValue = Int32(messageId) {
|
|
messagesDeleted.append(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageIdValue))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let aps = aps, let address = aps["addr"] as? String, let datacenterId = aps["dc"] as? Int {
|
|
var host = address
|
|
var port: Int32 = 443
|
|
if let range = address.range(of: ":") {
|
|
host = String(address[address.startIndex ..< range.lowerBound])
|
|
if let portValue = Int(String(address[range.upperBound...])) {
|
|
port = Int32(portValue)
|
|
}
|
|
}
|
|
var secret: Data?
|
|
if let secretString = aps["sec"] as? String {
|
|
let data = dataWithHexString(secretString)
|
|
if data.count == 16 || data.count == 32 {
|
|
secret = data
|
|
}
|
|
}
|
|
configurationUpdate = (Int32(datacenterId), host, port, secret)
|
|
}
|
|
|
|
if let aps = aps, let sound = aps["sound"] as? String {
|
|
apnsSound = sound
|
|
}
|
|
|
|
if payload["call_id"] != nil {
|
|
isCall = true
|
|
}
|
|
|
|
if payload["announcement"] != nil {
|
|
isAnnouncement = true
|
|
}
|
|
|
|
if let _ = body {
|
|
let _ = title
|
|
let _ = apnsSound
|
|
|
|
if isAnnouncement {
|
|
//presentAnnouncement
|
|
} else {
|
|
var peerId: PeerId?
|
|
|
|
shouldPollState = true
|
|
|
|
if let fromId = payload["from_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue)))
|
|
} else if let fromId = payload["chat_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue)))
|
|
} else if let fromId = payload["channel_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue)))
|
|
}
|
|
|
|
if let msgId = payload["msg_id"] {
|
|
let msgIdValue = msgId as! NSString
|
|
if let peerId = peerId {
|
|
notificationRequestId = .messageId(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(msgIdValue.intValue)))
|
|
}
|
|
} else if let randomId = payload["random_id"] {
|
|
let randomIdValue = randomId as! NSString
|
|
var peerId: PeerId?
|
|
if let encryptionIdString = payload["encryption_id"] as? String, let encryptionId = Int32(encryptionIdString) {
|
|
peerId = PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(encryptionId))
|
|
}
|
|
notificationRequestId = .globallyUniqueId(randomIdValue.longLongValue, peerId)
|
|
} else {
|
|
shouldPollState = true
|
|
}
|
|
}
|
|
} else if let _ = payload["max_id"] {
|
|
var peerId: PeerId?
|
|
|
|
if let fromId = payload["from_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue)))
|
|
} else if let fromId = payload["chat_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue)))
|
|
} else if let fromId = payload["channel_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue)))
|
|
}
|
|
|
|
if let peerId = peerId {
|
|
if let msgId = payload["max_id"] {
|
|
let msgIdValue = msgId as! NSString
|
|
if msgIdValue.intValue != 0 {
|
|
readMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(msgIdValue.intValue))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if isForcedLogOut {
|
|
self.clearNotificationsManager?.clearAll()
|
|
|
|
if let accountManager = self.accountManager {
|
|
let _ = logoutFromAccount(id: account.id, accountManager: accountManager, alreadyLoggedOutRemotely: true).start()
|
|
}
|
|
return
|
|
}
|
|
|
|
if notificationRequestId != nil || shouldPollState || isCall {
|
|
if !self.inForeground || !isCurrent {
|
|
self.beginPollingState(account: account)
|
|
}
|
|
}
|
|
if isLocationPolling {
|
|
if !self.inForeground || !isCurrent {
|
|
self.pollLiveLocationOnce(account.id)
|
|
}
|
|
}
|
|
|
|
if let readMessageId = readMessageId {
|
|
self.clearNotificationsManager?.append(readMessageId)
|
|
|
|
let _ = account.postbox.transaction(ignoreDisabled: true, { transaction -> Void in
|
|
transaction.applyIncomingReadMaxId(readMessageId)
|
|
}).start()
|
|
}
|
|
|
|
for messageId in messagesDeleted {
|
|
self.clearNotificationsManager?.append(messageId)
|
|
}
|
|
|
|
if !messagesDeleted.isEmpty {
|
|
let _ = account.postbox.transaction(ignoreDisabled: true, { transaction -> Void in
|
|
deleteMessages(transaction: transaction, mediaBox: account.postbox.mediaBox, ids: messagesDeleted)
|
|
}).start()
|
|
}
|
|
|
|
if readMessageId != nil || !messagesDeleted.isEmpty {
|
|
self.clearNotificationsManager?.commitNow()
|
|
}
|
|
|
|
if let (datacenterId, host, port, secret) = configurationUpdate {
|
|
account.network.mergeBackupDatacenterAddress(datacenterId: datacenterId, host: host, port: port, secret: secret)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var currentNotificationCall: (peer: Peer?, internalId: CallSessionInternalId)?
|
|
private func updateNotificationCall(call: (peer: Peer?, internalId: CallSessionInternalId)?, strings: PresentationStrings, nameOrder: PresentationPersonNameOrder) {
|
|
if let previousCall = currentNotificationCall {
|
|
if #available(iOS 10.0, *) {
|
|
let center = UNUserNotificationCenter.current()
|
|
center.removeDeliveredNotifications(withIdentifiers: ["call_\(previousCall.internalId)"])
|
|
} else {
|
|
if let notifications = self.application.scheduledLocalNotifications {
|
|
for notification in notifications {
|
|
if let userInfo = notification.userInfo, let callId = userInfo["callId"] as? String, callId == String(describing: previousCall.internalId) {
|
|
self.application.cancelLocalNotification(notification)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.currentNotificationCall = call
|
|
|
|
if let notificationCall = call {
|
|
let rawText = strings.PUSH_PHONE_CALL_REQUEST(notificationCall.peer?.displayTitle(strings: strings, displayOrder: nameOrder) ?? "").0
|
|
let title: String?
|
|
let body: String
|
|
if let index = rawText.firstIndex(of: "|") {
|
|
title = String(rawText[rawText.startIndex ..< index])
|
|
body = String(rawText[rawText.index(after: index)...])
|
|
} else {
|
|
title = nil
|
|
body = rawText
|
|
}
|
|
|
|
if #available(iOS 10.0, *) {
|
|
let content = UNMutableNotificationContent()
|
|
if let title = title {
|
|
content.title = title
|
|
}
|
|
content.body = body
|
|
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "0.m4a"))
|
|
content.categoryIdentifier = "incomingCall"
|
|
content.userInfo = [:]
|
|
|
|
let request = UNNotificationRequest(identifier: "call_\(notificationCall.internalId)", content: content, trigger: nil)
|
|
|
|
let center = UNUserNotificationCenter.current()
|
|
Logger.shared.log("NotificationManager", "adding call \(notificationCall.internalId)")
|
|
center.add(request, withCompletionHandler: { error in
|
|
if let error = error {
|
|
Logger.shared.log("NotificationManager", "error adding call \(notificationCall.internalId), error: \(String(describing: error))")
|
|
}
|
|
})
|
|
|
|
} else {
|
|
let notification = UILocalNotification()
|
|
if #available(iOS 8.2, *) {
|
|
notification.alertTitle = title
|
|
notification.alertBody = body
|
|
} else {
|
|
if let title = title {
|
|
notification.alertBody = "\(title): \(body)"
|
|
} else {
|
|
notification.alertBody = body
|
|
}
|
|
}
|
|
notification.category = "incomingCall"
|
|
notification.userInfo = ["callId": String(describing: notificationCall.internalId)]
|
|
notification.soundName = "0.m4a"
|
|
self.application.presentLocalNotificationNow(notification)
|
|
}
|
|
}
|
|
}
|
|
|
|
private let notificationCallStateDisposable = MetaDisposable()
|
|
private(set) var notificationCall: PresentationCall?
|
|
|
|
func setNotificationCall(_ call: PresentationCall?, strings: PresentationStrings) {
|
|
if self.notificationCall?.internalId != call?.internalId {
|
|
self.notificationCall = call
|
|
if let notificationCall = self.notificationCall {
|
|
let peer = notificationCall.peer
|
|
let internalId = notificationCall.internalId
|
|
let isIntegratedWithCallKit = notificationCall.isIntegratedWithCallKit
|
|
self.notificationCallStateDisposable.set((notificationCall.state
|
|
|> map { state -> (Peer?, CallSessionInternalId)? in
|
|
if isIntegratedWithCallKit {
|
|
return nil
|
|
}
|
|
if case .ringing = state.state {
|
|
return (peer, internalId)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|> distinctUntilChanged(isEqual: { $0?.1 == $1?.1 })).start(next: { [weak self] peerAndInternalId in
|
|
self?.updateNotificationCall(call: peerAndInternalId, strings: strings, nameOrder: .firstLast)
|
|
}))
|
|
} else {
|
|
self.notificationCallStateDisposable.set(nil)
|
|
self.updateNotificationCall(call: nil, strings: strings, nameOrder: .firstLast)
|
|
}
|
|
}
|
|
}
|
|
}
|