import PromiseKit // TODO: Move away from polling @objc(LKPublicChatPoller) public final class LokiPublicChatPoller : NSObject { private let publicChat: LokiPublicChat private var pollForNewMessagesTimer: Timer? = nil private var pollForDeletedMessagesTimer: Timer? = nil private var pollForModeratorsTimer: Timer? = nil private var pollForDisplayNamesTimer: Timer? = nil private var hasStarted = false private let userHexEncodedPublicKey = getUserHexEncodedPublicKey() // MARK: Settings private let pollForNewMessagesInterval: TimeInterval = 4 private let pollForDeletedMessagesInterval: TimeInterval = 20 private let pollForModeratorsInterval: TimeInterval = 10 * 60 private let pollForDisplayNamesInterval: TimeInterval = 60 // MARK: Lifecycle @objc(initForPublicChat:) public init(for publicChat: LokiPublicChat) { self.publicChat = publicChat super.init() } @objc public func startIfNeeded() { if hasStarted { return } pollForNewMessagesTimer = Timer.scheduledTimer(withTimeInterval: pollForNewMessagesInterval, repeats: true) { [weak self] _ in self?.pollForNewMessages() } pollForDeletedMessagesTimer = Timer.scheduledTimer(withTimeInterval: pollForDeletedMessagesInterval, repeats: true) { [weak self] _ in self?.pollForDeletedMessages() } pollForModeratorsTimer = Timer.scheduledTimer(withTimeInterval: pollForModeratorsInterval, repeats: true) { [weak self] _ in self?.pollForModerators() } pollForDisplayNamesTimer = Timer.scheduledTimer(withTimeInterval: pollForDisplayNamesInterval, repeats: true) { [weak self] _ in self?.pollForDisplayNames() } // Perform initial updates pollForNewMessages() pollForDeletedMessages() pollForModerators() pollForDisplayNames() hasStarted = true } @objc public func stop() { pollForNewMessagesTimer?.invalidate() pollForDeletedMessagesTimer?.invalidate() pollForModeratorsTimer?.invalidate() pollForDisplayNamesTimer?.invalidate() hasStarted = false } // MARK: Polling @objc(pollForNewMessages) public func objc_pollForNewMessages() -> AnyPromise { AnyPromise.from(pollForNewMessages()) } public func pollForNewMessages() -> Promise { let publicChat = self.publicChat let userHexEncodedPublicKey = self.userHexEncodedPublicKey return LokiPublicChatAPI.getMessages(for: publicChat.channel, on: publicChat.server).done(on: DispatchQueue.global()) { messages in let uniqueHexEncodedPublicKeys = Set(messages.map { $0.hexEncodedPublicKey }) func proceed() { let storage = OWSPrimaryStorage.shared() var newDisplayNameUpdatees: Set = [] storage.dbReadConnection.read { transaction in newDisplayNameUpdatees = Set(uniqueHexEncodedPublicKeys.filter { storage.getMasterHexEncodedPublicKey(for: $0, in: transaction) != $0 }.compactMap { storage.getMasterHexEncodedPublicKey(for: $0, in: transaction) }) } if !newDisplayNameUpdatees.isEmpty { let displayNameUpdatees = LokiPublicChatAPI.displayNameUpdatees[publicChat.id] ?? [] LokiPublicChatAPI.displayNameUpdatees[publicChat.id] = displayNameUpdatees.union(newDisplayNameUpdatees) } // Sorting the messages by timestamp before importing them fixes an issue where messages that quote older messages can't find those older messages messages.sorted { $0.timestamp < $1.timestamp }.forEach { message in var wasSentByCurrentUser = false OWSPrimaryStorage.shared().dbReadConnection.read { transaction in wasSentByCurrentUser = LokiDatabaseUtilities.isUserLinkedDevice(message.hexEncodedPublicKey, transaction: transaction) } var masterHexEncodedPublicKey: String? = nil storage.dbReadConnection.read { transaction in masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: message.hexEncodedPublicKey, in: transaction) } let senderHexEncodedPublicKey = masterHexEncodedPublicKey ?? message.hexEncodedPublicKey func generateDisplayName(from rawDisplayName: String) -> String { let endIndex = senderHexEncodedPublicKey.endIndex let cutoffIndex = senderHexEncodedPublicKey.index(endIndex, offsetBy: -8) return "\(rawDisplayName) (...\(senderHexEncodedPublicKey[cutoffIndex.. 0) { SSKEnvironment.shared.profileManager.updateProfileForContact(withID: masterHexEncodedPublicKey!, displayName: message.displayName, with: transaction) } SSKEnvironment.shared.profileManager.updateService(withProfileName: message.displayName, avatarUrl: avatar.url) SSKEnvironment.shared.profileManager.setProfileKeyData(avatar.profileKey, forRecipientId: masterHexEncodedPublicKey!, avatarURL: avatar.url) } } } } let hexEncodedPublicKeysToUpdate = uniqueHexEncodedPublicKeys.filter { hexEncodedPublicKey in let timeSinceLastUpdate: TimeInterval if let lastDeviceLinkUpdate = LokiAPI.lastDeviceLinkUpdate[hexEncodedPublicKey] { timeSinceLastUpdate = Date().timeIntervalSince(lastDeviceLinkUpdate) } else { timeSinceLastUpdate = .infinity } return timeSinceLastUpdate > LokiAPI.deviceLinkUpdateInterval } if !hexEncodedPublicKeysToUpdate.isEmpty { let storage = OWSPrimaryStorage.shared() storage.dbReadConnection.read { transaction in LokiStorageAPI.getDeviceLinks(associatedWith: hexEncodedPublicKeysToUpdate).done(on: DispatchQueue.global()) { _ in proceed() hexEncodedPublicKeysToUpdate.forEach { LokiAPI.lastDeviceLinkUpdate[$0] = Date() } }.catch(on: DispatchQueue.global()) { error in if case LokiDotNetAPI.Error.parsingFailed = error { // Don't immediately re-fetch in case of failure due to a parsing error hexEncodedPublicKeysToUpdate.forEach { LokiAPI.lastDeviceLinkUpdate[$0] = Date() } } proceed() } } } else { proceed() } } } private func pollForDeletedMessages() { let publicChat = self.publicChat let _ = LokiPublicChatAPI.getDeletedMessageServerIDs(for: publicChat.channel, on: publicChat.server).done(on: DispatchQueue.global()) { deletedMessageServerIDs in let storage = OWSPrimaryStorage.shared() storage.dbReadWriteConnection.readWrite { transaction in let deletedMessageIDs = deletedMessageServerIDs.compactMap { storage.getIDForMessage(withServerID: UInt($0), in: transaction) } deletedMessageIDs.forEach { messageID in TSMessage.fetch(uniqueId: messageID)?.remove(with: transaction) } } } } private func pollForModerators() { let _ = LokiPublicChatAPI.getModerators(for: publicChat.channel, on: publicChat.server) } private func pollForDisplayNames() { let _ = LokiPublicChatAPI.getDisplayNames(for: publicChat.channel, on: publicChat.server) } }