2022-04-21 08:42:35 +02:00
// C o p y r i g h t © 2 0 2 2 R a n g e p r o o f P t y L t d . A l l r i g h t s r e s e r v e d .
import Foundation
2022-12-02 07:38:01 +01:00
import Combine
2022-05-03 09:14:56 +02:00
import GRDB
2022-12-02 07:38:01 +01:00
import SignalCoreKit
2022-04-21 08:42:35 +02:00
import SessionUtilitiesKit
import SessionSnodeKit
public enum MessageSendJob : JobExecutor {
2022-04-27 02:48:54 +02:00
public static var maxFailureCount : Int = 10
2022-04-21 08:42:35 +02:00
public static var requiresThreadId : Bool = true
2022-04-22 10:47:11 +02:00
public static let requiresInteractionId : Bool = false // S o m e m e s s a g e s d o n ' t h a v e i n t e r a c t i o n s
2022-04-21 08:42:35 +02:00
public static func run (
_ job : Job ,
2022-06-24 10:29:45 +02:00
queue : DispatchQueue ,
2022-04-21 08:42:35 +02:00
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 {
2023-05-23 06:02:12 +02:00
SNLog ( " [MessageSendJob] Failing due to missing details " )
2023-03-17 05:42:40 +01:00
failure ( job , JobRunnerError . missingRequiredDetails , true )
2022-04-21 08:42:35 +02:00
return
}
2022-07-25 09:03:09 +02:00
// W e n e e d t o i n c l u d e ' f i l e I d s ' w h e n s e n d i n g m e s s a g e s w i t h a t t a c h m e n t s t o O p e n G r o u p s
// s o e x t r a c t t h e m f r o m a n y a s s o c i a t e d a t t a c h m e n t s
var messageFileIds : [ String ] = [ ]
2023-05-23 01:42:10 +02:00
// / E n s u r e a n y a s s o c i a t e d a t t a c h m e n t s h a v e a l r e a d y b e e n u p l o a d e d b e f o r e s e n d i n g t h e m e s s a g e
// /
// / * * N o t e : * * R e a c t i o n s r e f e r e n c e t h e i r o r i g i n a l m e s s a g e s o w e n e e d t o i g n o r e t h i s l o g i c f o r r e a c t i o n m e s s a g e s t o e n s u r e w e d o n ' t
// / i n c o r r e c t l y r e - u p l o a d i n c o m i n g a t t a c h m e n t s t h a t t h e u s e r r e a c t e d t o , w e a l s o w a n t t o e x c l u d e " s y n c " m e s s a g e s s i n c e t h e y s h o u l d
// / a l r e a d y h a v e a t t a c h m e n t s i n a v a l i d s t a t e
if
details . message is VisibleMessage ,
( details . message as ? VisibleMessage ) ? . reaction = = nil &&
details . isSyncMessage = = false
{
2022-04-21 08:42:35 +02:00
guard
2022-05-06 04:44:26 +02:00
let jobId : Int64 = job . id ,
let interactionId : Int64 = job . interactionId
2022-04-21 08:42:35 +02:00
else {
2023-05-23 06:02:12 +02:00
SNLog ( " [MessageSendJob] Failing due to missing details " )
2023-03-17 05:42:40 +01:00
failure ( job , JobRunnerError . missingRequiredDetails , true )
2022-04-21 08:42:35 +02:00
return
}
2022-07-25 09:03:09 +02:00
// I f t h e o r i g i n a l i n t e r a c t i o n n o l o n g e r e x i s t s t h e n d o n ' t b o t h e r s e n d i n g t h e m e s s a g e ( i e . t h e
// m e s s a g e w a s d e l e t e d b e f o r e i t e v e n g o t s e n t )
guard Storage . shared . read ( { db in try Interaction . exists ( db , id : interactionId ) } ) = = true else {
2023-05-23 06:02:12 +02:00
SNLog ( " [MessageSendJob] Failing due to missing interaction " )
2022-07-25 09:03:09 +02:00
failure ( job , StorageError . objectNotFound , true )
return
}
2022-05-06 10:07:57 +02:00
// C h e c k i f t h e r e a r e a n y a t t a c h m e n t s a s s o c i a t e d t o t h i s m e s s a g e , a n d i f s o
// u p l o a d t h e m n o w
//
// N o t e : N o r m a l a t t a c h m e n t s s h o u l d b e s e n t i n a n o n - d u r a b l e w a y b u t a n y
// a t t a c h m e n t s f o r L i n k P r e v i e w s a n d Q u o t e s w i l l b e p r o c e s s e d t h r o u g h t h i s m e c h a n i s m
2022-07-25 09:03:09 +02:00
let attachmentState : ( shouldFail : Bool , shouldDefer : Bool , fileIds : [ String ] ) ? = Storage . shared . write { db in
2022-05-06 10:07:57 +02:00
let allAttachmentStateInfo : [ Attachment . StateInfo ] = try Attachment
. stateInfo ( interactionId : interactionId )
. fetchAll ( db )
2022-07-25 09:03:09 +02:00
let maybeFileIds : [ String ? ] = allAttachmentStateInfo
2023-04-13 01:26:34 +02:00
. sorted { lhs , rhs in lhs . albumIndex < rhs . albumIndex }
2022-07-25 09:03:09 +02:00
. map { Attachment . fileId ( for : $0 . downloadUrl ) }
let fileIds : [ String ] = maybeFileIds . compactMap { $0 }
2022-05-06 10:07:57 +02:00
// I f t h e r e w e r e f a i l e d a t t a c h m e n t s t h e n t h i s j o b s h o u l d f a i l ( c a n ' t s e n d a
// m e s s a g e w h i c h h a s a s s o c i a t e d a t t a c h m e n t s i f t h e a t t a c h m e n t s f a i l t o u p l o a d )
2022-05-23 09:16:14 +02:00
guard ! allAttachmentStateInfo . contains ( where : { $0 . state = = . failedDownload } ) else {
2022-07-25 09:03:09 +02:00
return ( true , false , fileIds )
2022-05-06 10:07:57 +02:00
}
2022-05-29 11:26:06 +02:00
// C r e a t e j o b s f o r a n y p e n d i n g ( o r f a i l e d ) a t t a c h m e n t j o b s a n d i n s e r t t h e m i n t o t h e
2022-05-06 10:07:57 +02:00
// q u e u e b e f o r e t h e c u r r e n t j o b ( t h i s w i l l m e a n t h e c u r r e n t j o b w i l l r e - r u n
// a f t e r t h e s e i n s e r t e d j o b s c o m p l e t e )
//
// N o t e : I f t h e r e a r e a n y ' d o w n l o a d e d ' a t t a c h m e n t s t h e n t h e y a l s o n e e d t o b e
// u p l o a d e d ( a s a ' d o w n l o a d e d ' a t t a c h m e n t w i l l b e o n t h e c u r r e n t u s e r s d e v i c e
// b u t n o t o n t h e m e s s a g e r e c i p i e n t s d e v i c e - b o t h L i n k P r e v i e w a n d Q u o t e c a n
// h a v e t h i s c a s e )
try allAttachmentStateInfo
2022-08-04 05:25:46 +02:00
. filter { attachment -> Bool in
// N o n - m e d i a q u o t e s w o n ' t h a v e t h u m b n a i l s s o s o d o n ' t t r y t o u p l o a d t h e m
guard attachment . downloadUrl != Attachment . nonMediaQuoteFileId else { return false }
switch attachment . state {
case . uploading , . pendingDownload , . downloading , . failedUpload , . downloaded :
return true
default : return false
}
}
2022-05-29 11:26:06 +02:00
. filter { stateInfo in
// D o n ' t a d d a n e w j o b i f t h e r e i s o n e a l r e a d y i n t h e q u e u e
! JobRunner . hasPendingOrRunningJob (
with : . attachmentUpload ,
details : AttachmentUploadJob . Details (
messageSendJobId : jobId ,
attachmentId : stateInfo . attachmentId
)
)
}
2022-11-30 01:57:13 +01:00
. compactMap { stateInfo -> ( jobId : Int64 , job : Job ) ? in
2022-05-06 10:07:57 +02:00
JobRunner
. insert (
db ,
job : Job (
variant : . attachmentUpload ,
behaviour : . runOnce ,
threadId : job . threadId ,
interactionId : interactionId ,
details : AttachmentUploadJob . Details (
2022-05-13 10:07:24 +02:00
messageSendJobId : jobId ,
2022-05-06 10:07:57 +02:00
attachmentId : stateInfo . attachmentId
)
) ,
before : job
2022-11-30 01:57:13 +01:00
)
2022-05-06 10:07:57 +02:00
}
2022-11-30 01:57:13 +01:00
. forEach { otherJobId , _ in
2022-05-06 10:07:57 +02:00
// C r e a t e t h e d e p e n d e n c y b e t w e e n t h e j o b s
try JobDependencies (
jobId : jobId ,
dependantId : otherJobId
)
. insert ( db )
}
2022-04-21 08:42:35 +02:00
2022-05-06 10:07:57 +02:00
// I f t h e r e w e r e p e n d i n g o r u p l o a d i n g a t t a c h m e n t s t h e n s t o p h e r e ( w e w a n t t o
// u p l o a d t h e m f i r s t a n d t h e n r e - r u n t h i s s e n d j o b - t h e ' J o b R u n n e r . i n s e r t '
// m e t h o d w i l l t a k e c a r e o f t h i s )
2022-07-25 09:03:09 +02:00
let isMissingFileIds : Bool = ( maybeFileIds . count != fileIds . count )
let hasPendingUploads : Bool = allAttachmentStateInfo . contains ( where : { $0 . state != . uploaded } )
2022-05-06 10:07:57 +02:00
return (
2022-07-25 09:03:09 +02:00
( isMissingFileIds && ! hasPendingUploads ) ,
hasPendingUploads ,
fileIds
2022-05-06 10:07:57 +02:00
)
2022-04-21 08:42:35 +02:00
}
2022-05-06 10:07:57 +02:00
// D o n ' t s e n d m e s s a g e s w i t h f a i l e d a t t a c h m e n t u p l o a d s
//
// N o t e : I f w e h a v e g o t t e n t o t h i s p o i n t t h e n a n y d e p e n d a n t a t t a c h m e n t u p l o a d
// j o b s w i l l h a v e p e r m a n e n t l y f a i l e d s o t h i s m e s s a g e s e n d s h o u l d a l s o d o s o
guard attachmentState ? . shouldFail = = false else {
2023-05-23 06:02:12 +02:00
SNLog ( " [MessageSendJob] Failing due to failed attachment upload " )
2022-05-12 09:28:27 +02:00
failure ( job , AttachmentError . notUploaded , true )
2022-04-21 08:42:35 +02:00
return
}
2022-05-06 10:07:57 +02:00
// D e f e r t h e j o b i f w e f o u n d i n c o m p l e t e u p l o a d s
guard attachmentState ? . shouldDefer = = false else {
2023-05-23 06:02:12 +02:00
SNLog ( " [MessageSendJob] Deferring pending attachment uploads " )
2022-05-06 10:07:57 +02:00
deferred ( job )
return
}
2022-07-25 09:03:09 +02:00
// S t o r e t h e f i l e I d s s o t h e y c a n b e s e n t w i t h t h e o p e n g r o u p m e s s a g e c o n t e n t
messageFileIds = ( attachmentState ? . fileIds ? ? [ ] )
2022-04-21 08:42:35 +02:00
}
2022-06-09 10:37:44 +02:00
// S t o r e t h e s e n t T i m e s t a m p f r o m t h e m e s s a g e i n c a s e i t f a i l s d u e t o a c l o c k O u t O f S y n c e r r o r
let originalSentTimestamp : UInt64 ? = details . message . sentTimestamp
2022-12-05 03:52:39 +01:00
// / P e r f o r m t h e a c t u a l m e s s a g e s e n d i n g
// /
// / * * N o t e : * * N o n e e d t o u p l o a d a t t a c h m e n t s a s p a r t o f t h i s p r o c e s s a s t h e a b o v e l o g i c s p l i t s t h a t o u t i n t o i t ' s o w n j o b
// / s o w e s h o u l d n ' t g e t h e r e u n t i l a t t a c h m e n t s h a v e a l r e a d y b e e n u p l o a d e d
2022-11-27 22:32:32 +01:00
Storage . shared
2023-04-14 04:39:18 +02:00
. writePublisher { db in
2022-11-27 22:32:32 +01:00
try MessageSender . preparedSendData (
db ,
message : details . message ,
2022-12-05 07:39:40 +01:00
to : details . destination ,
2023-02-28 07:23:56 +01:00
namespace : details . destination . defaultNamespace ,
2023-03-06 07:53:30 +01:00
interactionId : job . interactionId ,
isSyncMessage : details . isSyncMessage
2022-11-27 22:32:32 +01:00
)
2022-04-21 08:42:35 +02:00
}
2022-12-05 07:39:40 +01:00
. map { sendData in sendData . with ( fileIds : messageFileIds ) }
2022-12-05 03:52:39 +01:00
. flatMap { MessageSender . sendImmediate ( preparedSendData : $0 ) }
2023-06-23 09:54:29 +02:00
. subscribe ( on : queue )
2023-02-20 02:56:48 +01:00
. receive ( on : queue )
2022-11-27 22:32:32 +01:00
. sinkUntilComplete (
receiveCompletion : { result in
switch result {
case . finished : success ( job , false )
case . failure ( let error ) :
2023-05-23 06:02:12 +02:00
SNLog ( " [MessageSendJob] Couldn't send message due to error: \( error ) . " )
2022-11-27 22:32:32 +01:00
switch error {
case let senderError as MessageSenderError where ! senderError . isRetryable :
failure ( job , error , true )
case OnionRequestAPIError . httpRequestFailedAtDestination ( let statusCode , _ , _ ) where statusCode = = 429 : // R a t e l i m i t e d
failure ( job , error , true )
case SnodeAPIError . clockOutOfSync :
2023-05-23 06:02:12 +02:00
SNLog ( " [MessageSendJob] \( originalSentTimestamp != nil ? " Permanently Failing " : " Failing " ) to send \( type ( of : details . message ) ) due to clock out of sync issue. " )
2022-11-27 22:32:32 +01:00
failure ( job , error , ( originalSentTimestamp != nil ) )
default :
2023-05-23 06:02:12 +02:00
SNLog ( " [MessageSendJob] Failed to send \( type ( of : details . message ) ) . " )
2022-11-27 22:32:32 +01:00
if details . message is VisibleMessage {
guard
let interactionId : Int64 = job . interactionId ,
Storage . shared . read ( { db in try Interaction . exists ( db , id : interactionId ) } ) = = true
else {
// T h e m e s s a g e h a s b e e n d e l e t e d s o p e r m a n e n t l y f a i l t h e j o b
failure ( job , error , true )
return
}
}
failure ( job , error , false )
}
}
}
)
2022-04-21 08:42:35 +02:00
}
}
// MARK: - M e s s a g e S e n d J o b . D e t a i l s
extension MessageSendJob {
public struct Details : Codable {
private enum CodingKeys : String , CodingKey {
case destination
case message
2023-02-14 03:41:24 +01:00
case isSyncMessage
2022-06-03 07:47:16 +02:00
case variant
2022-04-21 08:42:35 +02:00
}
public let destination : Message . Destination
public let message : Message
2023-03-06 07:53:30 +01:00
public let isSyncMessage : Bool
2022-06-03 07:47:16 +02:00
public let variant : Message . Variant ?
2022-04-21 08:42:35 +02:00
// MARK: - I n i t i a l i z a t i o n
public init (
destination : Message . Destination ,
2023-02-14 03:41:24 +01:00
message : Message ,
2023-03-06 07:53:30 +01:00
isSyncMessage : Bool = false
2022-04-21 08:42:35 +02:00
) {
self . destination = destination
self . message = message
2023-02-14 03:41:24 +01:00
self . isSyncMessage = isSyncMessage
2022-06-03 07:47:16 +02:00
self . variant = Message . Variant ( from : message )
2022-04-21 08:42:35 +02:00
}
2022-06-03 07:47:16 +02:00
// MARK: - C o d a b l e
2022-04-21 08:42:35 +02:00
public init ( from decoder : Decoder ) throws {
let container : KeyedDecodingContainer < CodingKeys > = try decoder . container ( keyedBy : CodingKeys . self )
2022-06-03 07:47:16 +02:00
guard let variant : Message . Variant = try ? container . decode ( Message . Variant . self , forKey : . variant ) else {
SNLog ( " Unable to decode messageSend job due to missing variant " )
2022-05-30 05:04:26 +02:00
throw StorageError . decodingFailed
2022-04-21 08:42:35 +02:00
}
self = Details (
destination : try container . decode ( Message . Destination . self , forKey : . destination ) ,
2023-02-14 03:41:24 +01:00
message : try variant . decode ( from : container , forKey : . message ) ,
2023-03-06 07:53:30 +01:00
isSyncMessage : ( ( try ? container . decode ( Bool . self , forKey : . isSyncMessage ) ) ? ? false )
2022-04-21 08:42:35 +02:00
)
}
public func encode ( to encoder : Encoder ) throws {
var container : KeyedEncodingContainer < CodingKeys > = encoder . container ( keyedBy : CodingKeys . self )
2022-06-03 07:47:16 +02:00
guard let variant : Message . Variant = Message . Variant ( from : message ) else {
SNLog ( " Unable to encode messageSend job due to unsupported variant " )
2022-05-30 05:04:26 +02:00
throw StorageError . objectNotFound
2022-04-21 08:42:35 +02:00
}
try container . encode ( destination , forKey : . destination )
try container . encode ( message , forKey : . message )
2023-03-06 07:53:30 +01:00
try container . encode ( isSyncMessage , forKey : . isSyncMessage )
2022-06-03 07:47:16 +02:00
try container . encode ( variant , forKey : . variant )
2022-04-21 08:42:35 +02:00
}
}
}