import PromiseKit extension Storage { /// Returns the ID of the thread. public func getOrCreateThread(for publicKey: String, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? { let transaction = transaction as! YapDatabaseReadWriteTransaction var threadOrNil: TSThread? if let openGroupID = openGroupID { if let threadID = Storage.shared.v2GetThreadID(for: openGroupID), let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) { threadOrNil = thread } } else if let groupPublicKey = groupPublicKey { guard Storage.shared.isClosedGroup(groupPublicKey) else { return nil } let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) threadOrNil = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) } else { threadOrNil = TSContactThread.getOrCreateThread(withContactSessionID: publicKey, transaction: transaction) } return threadOrNil?.uniqueId } /// Returns the ID of the `TSIncomingMessage` that was constructed. public func persist(_ message: VisibleMessage, quotedMessage: TSQuotedMessage?, linkPreview: OWSLinkPreview?, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? { let transaction = transaction as! YapDatabaseReadWriteTransaction guard let threadID = getOrCreateThread(for: message.syncTarget ?? message.sender!, groupPublicKey: groupPublicKey, openGroupID: openGroupID, using: transaction), let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return nil } let tsMessage: TSMessage if message.sender == getUserPublicKey() { let tsOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction) var recipients: [String] = [] if let syncTarget = message.syncTarget { recipients.append(syncTarget) } else if let thread = thread as? TSGroupThread, thread.isClosedGroup { recipients = thread.groupModel.groupMemberIds } recipients.forEach { recipient in tsOutgoingMessage.update(withSentRecipient: recipient, wasSentByUD: true, transaction: transaction) } tsMessage = tsOutgoingMessage } else { tsMessage = TSIncomingMessage.from(message, quotedMessage: quotedMessage, linkPreview: linkPreview, associatedWith: thread) } tsMessage.save(with: transaction) tsMessage.attachments(with: transaction).forEach { attachment in attachment.albumMessageId = tsMessage.uniqueId! attachment.save(with: transaction) } return tsMessage.uniqueId! } /// Returns the IDs of the saved attachments. public func persist(_ attachments: [VisibleMessage.Attachment], using transaction: Any) -> [String] { return attachments.map { attachment in let tsAttachment = TSAttachmentPointer.from(attachment) tsAttachment.save(with: transaction as! YapDatabaseReadWriteTransaction) return tsAttachment.uniqueId! } } /// Also touches the associated message. public func setAttachmentState(to state: TSAttachmentPointerState, for pointer: TSAttachmentPointer, associatedWith tsMessageID: String, using transaction: Any) { let transaction = transaction as! YapDatabaseReadWriteTransaction // Workaround for some YapDatabase funkiness where pointer at this point can actually be a TSAttachmentStream guard pointer.responds(to: #selector(setter: TSAttachmentPointer.state)) else { return } pointer.state = state pointer.save(with: transaction) guard let tsMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) else { return } MessageInvalidator.invalidate(tsMessage, with: transaction) } /// Also touches the associated message. public func persist(_ stream: TSAttachmentStream, associatedWith tsMessageID: String, using transaction: Any) { let transaction = transaction as! YapDatabaseReadWriteTransaction stream.save(with: transaction) guard let tsMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) else { return } MessageInvalidator.invalidate(tsMessage, with: transaction) } private static let receivedMessageTimestampsCollection = "ReceivedMessageTimestampsCollection" public func getReceivedMessageTimestamps(using transaction: Any) -> [UInt64] { var result: [UInt64] = [] let transaction = transaction as! YapDatabaseReadWriteTransaction transaction.enumerateRows(inCollection: Storage.receivedMessageTimestampsCollection) { _, object, _, _ in guard let timestamps = object as? [UInt64] else { return } result = timestamps } return result } public func removeReceivedMessageTimestamps(_ timestamps: Set, using transaction: Any) { var receivedMessageTimestamps = getReceivedMessageTimestamps(using: transaction) timestamps.forEach { timestamp in guard let index = receivedMessageTimestamps.firstIndex(of: timestamp) else { return } receivedMessageTimestamps.remove(at: index) } let transaction = transaction as! YapDatabaseReadWriteTransaction transaction.setObject(receivedMessageTimestamps, forKey: "receivedMessageTimestamps", inCollection: Storage.receivedMessageTimestampsCollection) } public func addReceivedMessageTimestamp(_ timestamp: UInt64, using transaction: Any) { var receivedMessageTimestamps = getReceivedMessageTimestamps(using: transaction) // TODO: Do we need to sort the timestamps here? if receivedMessageTimestamps.count > 1000 { receivedMessageTimestamps.remove(at: 0) } // Limit the size of the collection to 1000 receivedMessageTimestamps.append(timestamp) let transaction = transaction as! YapDatabaseReadWriteTransaction transaction.setObject(receivedMessageTimestamps, forKey: "receivedMessageTimestamps", inCollection: Storage.receivedMessageTimestampsCollection) } }