session-ios/SessionMessagingKit/Jobs/Types/MessageSendJob.swift
Morgan Pretty aabf656d89 Finished off the MediaGallery logic
Updated the config message generation for GRDB
Migrated more preferences into GRDB
Added paging to the MediaTileViewController and sorted out the various animations/transitions
Fixed an issue where the 'recipientState' for the 'baseQuery' on the ConversationCell.ViewModel wasn't grouping correctly
Fixed an issue where the MediaZoomAnimationController could fail if the contextual info wasn't available
Fixed an issue where the MediaZoomAnimationController bounce looked odd when returning to the detail screen from the tile screen
Fixed an issue where the MediaZoomAnimationController didn't work for videos
Fixed a bug where the YDB to GRDB migration wasn't properly handling video files
Fixed a number of minor UI bugs with the GalleryRailView
Deleted a bunch of legacy code
2022-05-20 17:58:39 +10:00

243 lines
10 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import PromiseKit
import SignalCoreKit
import SessionUtilitiesKit
import SessionSnodeKit
public enum MessageSendJob: JobExecutor {
public static var maxFailureCount: Int = 10
public static var requiresThreadId: Bool = true
public static let requiresInteractionId: Bool = false // Some messages don't have interactions
public static func run(
_ job: Job,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
) {
guard
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
return
}
if details.message is VisibleMessage {
guard
let jobId: Int64 = job.id,
let interactionId: Int64 = job.interactionId
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
return
}
// Check if there are any attachments associated to this message, and if so
// upload them now
//
// Note: Normal attachments should be sent in a non-durable way but any
// attachments for LinkPreviews and Quotes will be processed through this mechanism
let attachmentState: (shouldFail: Bool, shouldDefer: Bool)? = GRDBStorage.shared.write { db in
let allAttachmentStateInfo: [Attachment.StateInfo] = try Attachment
.stateInfo(interactionId: interactionId)
.fetchAll(db)
// If there were failed attachments then this job should fail (can't send a
// message which has associated attachments if the attachments fail to upload)
guard !allAttachmentStateInfo.contains(where: { $0.state == .failed }) else {
return (true, false)
}
// Create jobs for any pending attachment jobs and insert them into the
// queue before the current job (this will mean the current job will re-run
// after these inserted jobs complete)
//
// Note: If there are any 'downloaded' attachments then they also need to be
// uploaded (as a 'downloaded' attachment will be on the current users device
// but not on the message recipients device - both LinkPreview and Quote can
// have this case)
try allAttachmentStateInfo
.filter { $0.state == .pending || $0.state == .downloaded }
.compactMap { stateInfo in
JobRunner
.insert(
db,
job: Job(
variant: .attachmentUpload,
behaviour: .runOnce,
threadId: job.threadId,
interactionId: interactionId,
details: AttachmentUploadJob.Details(
messageSendJobId: jobId,
attachmentId: stateInfo.attachmentId
)
),
before: job
)?
.id
}
.forEach { otherJobId in
// Create the dependency between the jobs
try JobDependencies(
jobId: jobId,
dependantId: otherJobId
)
.insert(db)
}
// If there were pending or uploading attachments then stop here (we want to
// upload them first and then re-run this send job - the 'JobRunner.insert'
// method will take care of this)
return (
false,
allAttachmentStateInfo.contains(where: { $0.state != .uploaded })
)
}
// Don't send messages with failed attachment uploads
//
// Note: If we have gotten to this point then any dependant attachment upload
// jobs will have permanently failed so this message send should also do so
guard attachmentState?.shouldFail == false else {
failure(job, AttachmentError.notUploaded, true)
return
}
// Defer the job if we found incomplete uploads
guard attachmentState?.shouldDefer == false else {
deferred(job)
return
}
}
// Add the threadId to the message if there isn't one set
details.message.threadId = (details.message.threadId ?? job.threadId)
// Perform the actual message sending
GRDBStorage.shared.write { db -> Promise<Void> in
try MessageSender.sendImmediate(
db,
message: details.message,
to: details.destination,
interactionId: job.interactionId
)
}
.done2 { _ in success(job, false) }
.catch2 { error in
SNLog("Couldn't send message due to error: \(error).")
switch error {
case let senderError as MessageSenderError where !senderError.isRetryable:
failure(job, error, true)
case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _, _) where statusCode == 429: // Rate limited
failure(job, error, true)
default:
SNLog("Failed to send \(type(of: details.message)).")
if details.message is VisibleMessage {
guard
let interactionId: Int64 = job.interactionId,
GRDBStorage.shared.read({ db in try Interaction.exists(db, id: interactionId) }) == true
else {
// The message has been deleted so permanently fail the job
failure(job, error, true)
return
}
}
failure(job, error, false)
}
}
}
}
// MARK: - MessageSendJob.Details
extension MessageSendJob {
public struct Details: Codable {
// Note: This approach is less than ideal (since it needs to be manually maintained) but
// I couldn't think of an easy way to support a generic decoded type for the 'message'
// value in the database while using Codable
private static let supportedMessageTypes: [String: Message.Type] = [
"VisibleMessage": VisibleMessage.self,
"ReadReceipt": ReadReceipt.self,
"TypingIndicator": TypingIndicator.self,
"ClosedGroupControlMessage": ClosedGroupControlMessage.self,
"DataExtractionNotification": DataExtractionNotification.self,
"ExpirationTimerUpdate": ExpirationTimerUpdate.self,
"ConfigurationMessage": ConfigurationMessage.self,
"UnsendRequest": UnsendRequest.self,
"MessageRequestResponse": MessageRequestResponse.self
]
private enum CodingKeys: String, CodingKey {
case interactionId
case destination
case messageType
case message
}
public let destination: Message.Destination
public let message: Message
// MARK: - Initialization
public init(
destination: Message.Destination,
message: Message
) {
self.destination = destination
self.message = message
}
public init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
guard let messageType: String = try? container.decode(String.self, forKey: .messageType) else {
Logger.error("Unable to decode messageSend job due to missing messageType")
throw GRDBStorageError.decodingFailed
}
/// Note: This **MUST** be a `Codable.Type` rather than a `Message.Type` otherwise the decoding will result
/// in a `Message` object being returned rather than the desired subclass
guard let MessageType: Codable.Type = MessageSendJob.Details.supportedMessageTypes[messageType] else {
Logger.error("Unable to decode messageSend job due to unsupported messageType")
throw GRDBStorageError.decodingFailed
}
guard let message: Message = try MessageType.decoded(with: container, forKey: .message) as? Message else {
Logger.error("Unable to decode messageSend job due to message conversion issue")
throw GRDBStorageError.decodingFailed
}
self = Details(
destination: try container.decode(Message.Destination.self, forKey: .destination),
message: message
)
}
public func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
let messageType: Codable.Type = type(of: message)
let maybeMessageTypeString: String? = MessageSendJob.Details.supportedMessageTypes
.first(where: { _, type in messageType == type })?
.key
guard let messageTypeString: String = maybeMessageTypeString else {
Logger.error("Unable to encode messageSend job due to unsupported messageType")
throw GRDBStorageError.objectNotFound
}
try container.encode(destination, forKey: .destination)
try container.encode(messageTypeString, forKey: .messageType)
try container.encode(message, forKey: .message)
}
}
}