import PromiseKit @objc(SNOpenGroupManagerV2) public final class OpenGroupManagerV2 : NSObject { private var pollers: [String:OpenGroupPollerV2] = [:] // One for each server private var isPolling = false // MARK: Initialization @objc public static let shared = OpenGroupManagerV2() private override init() { } // MARK: Polling @objc public func startPolling() { guard !isPolling else { return } isPolling = true let servers = Set(Storage.shared.getAllV2OpenGroups().values.map { $0.server }) servers.forEach { server in if let poller = pollers[server] { poller.stop() } // Should never occur let poller = OpenGroupPollerV2(for: server) poller.startIfNeeded() pollers[server] = poller } } @objc public func stopPolling() { pollers.forEach { (_, openGroupPoller) in openGroupPoller.stop() } pollers.removeAll() } // MARK: Adding & Removing public func add(room: String, server: String, publicKey: String, using transaction: Any) -> Promise { // If we are currently polling for this server and already have a TSGroupThread for this room the do nothing let transaction = transaction as! YapDatabaseReadWriteTransaction let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData("\(server).\(room)") if OpenGroupManagerV2.shared.pollers[server] != nil && TSGroupThread.fetch(groupId: groupId, transaction: transaction) != nil { SNLog("Ignoring join open group attempt (already joined)") return Promise.value(()) } let storage = Storage.shared // Clear any existing data if needed storage.removeLastMessageServerID(for: room, on: server, using: transaction) storage.removeLastDeletionServerID(for: room, on: server, using: transaction) storage.removeAuthToken(for: room, on: server, using: transaction) // Store the public key storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction) let (promise, seal) = Promise.pending() transaction.addCompletionQueue(DispatchQueue.global(qos: .userInitiated)) { // Get the group info OpenGroupAPIV2.getInfo(for: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { info in // Create the open group model and the thread let openGroup = OpenGroupV2(server: server, room: room, name: info.name, publicKey: publicKey, imageID: info.imageID) let groupID = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id) let model = TSGroupModel(title: openGroup.name, memberIds: [ getUserHexEncodedPublicKey() ], image: nil, groupId: groupID, groupType: .openGroup, adminIds: []) // Store everything storage.write(with: { transaction in let transaction = transaction as! YapDatabaseReadWriteTransaction let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) thread.shouldBeVisible = true thread.save(with: transaction) storage.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction) }, completion: { // Start the poller if needed if OpenGroupManagerV2.shared.pollers[server] == nil { let poller = OpenGroupPollerV2(for: server) poller.startIfNeeded() OpenGroupManagerV2.shared.pollers[server] = poller } // Fetch the group image OpenGroupAPIV2.getGroupImage(for: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { data in storage.write { transaction in // Update the thread let transaction = transaction as! YapDatabaseReadWriteTransaction let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) thread.groupModel.groupImage = UIImage(data: data) thread.save(with: transaction) } }.retainUntilComplete() // Finish seal.fulfill(()) }) }.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in seal.reject(error) } } return promise } public func delete(_ openGroup: OpenGroupV2, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) { let storage = SNMessagingKitConfiguration.shared.storage // Stop the poller if needed let openGroups = storage.getAllV2OpenGroups().values.filter { $0.server == openGroup.server } if openGroups.count == 1 && openGroups.last == openGroup { let poller = pollers[openGroup.server] poller?.stop() pollers[openGroup.server] = nil } // Remove all data var messageIDs: Set = [] var messageTimestamps: Set = [] thread.enumerateInteractions(with: transaction) { interaction, _ in messageIDs.insert(interaction.uniqueId!) messageTimestamps.insert(interaction.timestamp) } storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction) Storage.shared.removeReceivedMessageTimestamps(messageTimestamps, using: transaction) Storage.shared.removeLastMessageServerID(for: openGroup.room, on: openGroup.server, using: transaction) Storage.shared.removeLastDeletionServerID(for: openGroup.room, on: openGroup.server, using: transaction) let _ = OpenGroupAPIV2.deleteAuthToken(for: openGroup.room, on: openGroup.server) Storage.shared.removeOpenGroupPublicKey(for: openGroup.server, using: transaction) thread.removeAllThreadInteractions(with: transaction) thread.remove(with: transaction) Storage.shared.removeV2OpenGroup(for: thread.uniqueId!, using: transaction) } // MARK: Convenience public static func parseV2OpenGroup(from string: String) -> (room: String, server: String, publicKey: String)? { guard let url = URL(string: string), let host = url.host ?? given(string.split(separator: "/").first, { String($0) }), let query = url.query else { return nil } // Inputs that should work: // https://sessionopengroup.co/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c // http://sessionopengroup.co/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c // sessionopengroup.co/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c (does NOT go to HTTPS) // https://143.198.213.225:443/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c // 143.198.213.255:80/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c let useTLS = (url.scheme == "https") let updatedPath = (url.path.starts(with: "/r/") ? url.path.substring(from: 2) : url.path) let room = String(updatedPath.dropFirst()) // Drop the leading slash let queryParts = query.split(separator: "=") guard !room.isEmpty && !room.contains("/"), queryParts.count == 2, queryParts[0] == "public_key" else { return nil } let publicKey = String(queryParts[1]) guard publicKey.count == 64 && Hex.isValid(publicKey) else { return nil } var server = (useTLS ? "https://" : "http://") + host if let port = url.port { server += ":\(port)" } return (room: room, server: server, publicKey: publicKey) } }