// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation import GRDB import PromiseKit import SessionUtilitiesKit extension MessageSender { // MARK: - Durable public static func send(_ db: Database, interaction: Interaction, with attachments: [SignalAttachment], in thread: SessionThread) throws { guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } try prep(db, signalAttachments: attachments, for: interactionId) send( db, message: VisibleMessage.from(db, interaction: interaction), threadId: thread.id, interactionId: interactionId, to: try Message.Destination.from(db, thread: thread) ) } public static func send(_ db: Database, interaction: Interaction, in thread: SessionThread) throws { // Only 'VisibleMessage' types can be sent via this method guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage } guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } send( db, message: VisibleMessage.from(db, interaction: interaction), threadId: thread.id, interactionId: interactionId, to: try Message.Destination.from(db, thread: thread) ) } public static func send(_ db: Database, message: Message, interactionId: Int64?, in thread: SessionThread) throws { send( db, message: message, threadId: thread.id, interactionId: interactionId, to: try Message.Destination.from(db, thread: thread) ) } public static func send(_ db: Database, message: Message, threadId: String?, interactionId: Int64?, to destination: Message.Destination) { JobRunner.add( db, job: Job( variant: .messageSend, threadId: threadId, interactionId: interactionId, details: MessageSendJob.Details( destination: destination, message: message ) ) ) } // MARK: - Non-Durable public static func sendNonDurably(_ db: Database, interaction: Interaction, with attachments: [SignalAttachment], in thread: SessionThread) throws -> Promise { guard let interactionId: Int64 = interaction.id else { return Promise(error: StorageError.objectNotSaved) } try prep(db, signalAttachments: attachments, for: interactionId) return sendNonDurably( db, message: VisibleMessage.from(db, interaction: interaction), interactionId: interactionId, to: try Message.Destination.from(db, thread: thread) ) } public static func sendNonDurably(_ db: Database, interaction: Interaction, in thread: SessionThread) throws -> Promise { // Only 'VisibleMessage' types can be sent via this method guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage } guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } return sendNonDurably( db, message: VisibleMessage.from(db, interaction: interaction), interactionId: interactionId, to: try Message.Destination.from(db, thread: thread) ) } public static func sendNonDurably(_ db: Database, message: Message, interactionId: Int64?, in thread: SessionThread) throws -> Promise { return sendNonDurably( db, message: message, interactionId: interactionId, to: try Message.Destination.from(db, thread: thread) ) } public static func sendNonDurably(_ db: Database, message: Message, interactionId: Int64?, to destination: Message.Destination) -> Promise { var attachmentUploadPromises: [Promise] = [Promise.value(())] // If we have an interactionId then check if it has any attachments and process them first if let interactionId: Int64 = interactionId { let threadId: String = { switch destination { case .contact(let publicKey): return publicKey case .closedGroup(let groupPublicKey): return groupPublicKey case .openGroupV2(let room, let server): return OpenGroup.idFor(room: room, server: server) case .openGroup: return "" } }() let openGroup: OpenGroup? = try? OpenGroup.fetchOne(db, id: threadId) let attachmentStateInfo: [Attachment.StateInfo] = (try? Attachment .stateInfo(interactionId: interactionId, state: .uploading) .fetchAll(db)) .defaulting(to: []) attachmentUploadPromises = (try? Attachment .filter(ids: attachmentStateInfo.map { $0.attachmentId }) .fetchAll(db)) .defaulting(to: []) .map { attachment -> Promise in let (promise, seal) = Promise.pending() attachment.upload( db, using: { data in if let openGroup: OpenGroup = openGroup { return OpenGroupAPIV2.upload(data, to: openGroup.room, on: openGroup.server) } return FileServerAPIV2.upload(data) }, encrypt: (openGroup == nil), success: { seal.fulfill(()) }, failure: { seal.reject($0) } ) return promise } } // Once the attachments are processed then send the message return when(resolved: attachmentUploadPromises) .then { results -> Promise in let errors: [Error] = results .compactMap { result -> Error? in if case .rejected(let error) = result { return error } return nil } if let error: Error = errors.first { return Promise(error: error) } return GRDBStorage.shared.write { db in try MessageSender.sendImmediate( db, message: message, to: destination, interactionId: interactionId ) } } } /// This method requires the `db` value to be passed in because if it's called within a `writeAsync` completion block /// it will throw a "re-entrant" fatal error when attempting to write again public static func syncConfiguration(_ db: Database, forceSyncNow: Bool = true) throws -> Promise { // If we don't have a userKeyPair yet then there is no need to sync the configuration // as the user doesn't exist yet (this will get triggered on the first launch of a // fresh install due to the migrations getting run) guard Identity.userExists(db) else { return Promise(error: StorageError.generic) } let destination: Message.Destination = Message.Destination.contact( publicKey: getUserHexEncodedPublicKey(db) ) let configurationMessage = try ConfigurationMessage.getCurrent(db) let (promise, seal) = Promise.pending() if forceSyncNow { try MessageSender .sendImmediate(db, message: configurationMessage, to: destination, interactionId: nil) .done { seal.fulfill(()) } .catch { _ in seal.reject(StorageError.generic) } .retainUntilComplete() } else { JobRunner.add( db, job: Job( variant: .messageSend, details: MessageSendJob.Details( destination: destination, message: configurationMessage ) ) ) seal.fulfill(()) } return promise } }