2022-01-04 22:24:18 +00:00
// Copyright 2021-2022 Signal Messenger, LLC
2021-08-31 20:58:39 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2021-10-29 23:19:44 +00:00
import type PQueue from 'p-queue' ;
2021-09-17 18:27:53 +00:00
import type { LoggerType } from '../types/Logging' ;
2021-08-31 20:58:39 +00:00
import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff' ;
import { commonShouldJobContinue } from './helpers/commonShouldJobContinue' ;
2021-10-29 23:19:44 +00:00
import { InMemoryQueues } from './helpers/InMemoryQueues' ;
2021-09-02 17:44:54 +00:00
import type { MessageModel } from '../models/messages' ;
import { getMessageById } from '../messages/getMessageById' ;
2021-08-31 20:58:39 +00:00
import type { ConversationModel } from '../models/conversations' ;
import { ourProfileKeyService } from '../services/ourProfileKey' ;
import { strictAssert } from '../util/assert' ;
import { isRecord } from '../util/isRecord' ;
import * as durations from '../util/durations' ;
import { isMe } from '../util/whatTypeOfConversation' ;
import { getSendOptions } from '../util/getSendOptions' ;
import { SignalService as Proto } from '../protobuf' ;
import { handleMessageSend } from '../util/handleMessageSend' ;
import type { CallbackResultType } from '../textsecure/Types.d' ;
import { isSent } from '../messages/MessageSendState' ;
import { getLastChallengeError , isOutgoing } from '../state/selectors/message' ;
2021-10-29 23:19:44 +00:00
import type { AttachmentType } from '../textsecure/SendMessage' ;
2021-10-05 22:10:08 +00:00
import type { LinkPreviewType } from '../types/message/LinkPreviews' ;
2021-08-31 20:58:39 +00:00
import type { BodyRangesType } from '../types/Util' ;
import type { WhatIsThis } from '../window.d' ;
import { JobQueue } from './JobQueue' ;
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore' ;
2022-01-12 00:50:11 +00:00
import { handleMultipleSendErrors } from './helpers/handleMultipleSendErrors' ;
2021-08-31 20:58:39 +00:00
2021-11-11 22:43:05 +00:00
const { loadAttachmentData , loadPreviewData , loadQuoteData , loadStickerData } =
window . Signal . Migrations ;
2021-08-31 20:58:39 +00:00
const { Message } = window . Signal . Types ;
const MAX_RETRY_TIME = durations . DAY ;
const MAX_ATTEMPTS = exponentialBackoffMaxAttempts ( MAX_RETRY_TIME ) ;
type NormalMessageSendJobData = {
messageId : string ;
conversationId : string ;
} ;
export class NormalMessageSendJobQueue extends JobQueue < NormalMessageSendJobData > {
2021-10-29 23:19:44 +00:00
private readonly inMemoryQueues = new InMemoryQueues ( ) ;
2021-08-31 20:58:39 +00:00
protected parseData ( data : unknown ) : NormalMessageSendJobData {
// Because we do this so often and Zod is a bit slower, we do "manual" parsing here.
strictAssert ( isRecord ( data ) , 'Job data is not an object' ) ;
const { messageId , conversationId } = data ;
strictAssert (
typeof messageId === 'string' ,
'Job data had a non-string message ID'
) ;
strictAssert (
typeof conversationId === 'string' ,
'Job data had a non-string conversation ID'
) ;
return { messageId , conversationId } ;
}
2021-11-12 23:44:20 +00:00
protected override getInMemoryQueue ( {
2021-09-07 20:39:14 +00:00
data ,
} : Readonly < { data : NormalMessageSendJobData } > ) : PQueue {
2021-10-29 23:19:44 +00:00
return this . inMemoryQueues . get ( data . conversationId ) ;
2021-08-31 20:58:39 +00:00
}
protected async run (
{
data ,
timestamp ,
} : Readonly < { data : NormalMessageSendJobData ; timestamp : number } > ,
{ attempt , log } : Readonly < { attempt : number ; log : LoggerType } >
) : Promise < void > {
2021-09-07 20:39:14 +00:00
const { messageId } = data ;
2021-08-31 20:58:39 +00:00
2021-09-07 20:39:14 +00:00
const timeRemaining = timestamp + MAX_RETRY_TIME - Date . now ( ) ;
const isFinalAttempt = attempt >= MAX_ATTEMPTS ;
2021-08-31 20:58:39 +00:00
2021-09-07 20:39:14 +00:00
// We don't immediately use this value because we may want to mark the message
// failed before doing so.
const shouldContinue = await commonShouldJobContinue ( {
attempt ,
log ,
timeRemaining ,
} ) ;
2021-08-31 20:58:39 +00:00
2021-11-04 21:11:47 +00:00
await window . ConversationController . load ( ) ;
2021-08-31 20:58:39 +00:00
2021-09-07 20:39:14 +00:00
const message = await getMessageById ( messageId ) ;
if ( ! message ) {
log . info (
` message ${ messageId } was not found, maybe because it was deleted. Giving up on sending it `
) ;
return ;
}
if ( ! isOutgoing ( message . attributes ) ) {
log . error (
` message ${ messageId } was not an outgoing message to begin with. This is probably a bogus job. Giving up on sending it `
) ;
return ;
}
if ( message . isErased ( ) || message . get ( 'deletedForEveryone' ) ) {
log . info ( ` message ${ messageId } was erased. Giving up on sending it ` ) ;
return ;
}
2021-08-31 20:58:39 +00:00
2021-09-07 20:39:14 +00:00
let messageSendErrors : Array < Error > = [ ] ;
// We don't want to save errors on messages unless we're giving up. If it's our
// final attempt, we know upfront that we want to give up. However, we might also
// want to give up if (1) we get a 508 from the server, asking us to please stop
// (2) we get a 428 from the server, flagging the message for spam (3) some other
// reason not known at the time of this writing.
//
// This awkward callback lets us hold onto errors we might want to save, so we can
// decide whether to save them later on.
const saveErrors = isFinalAttempt
? undefined
: ( errors : Array < Error > ) = > {
messageSendErrors = errors ;
} ;
if ( ! shouldContinue ) {
log . info ( ` message ${ messageId } ran out of time. Giving up on sending it ` ) ;
await markMessageFailed ( message , messageSendErrors ) ;
return ;
}
try {
const conversation = message . getConversation ( ) ;
if ( ! conversation ) {
throw new Error (
` could not find conversation for message with ID ${ messageId } `
2021-08-31 20:58:39 +00:00
) ;
}
2021-09-07 20:39:14 +00:00
const {
allRecipientIdentifiers ,
recipientIdentifiersWithoutMe ,
untrustedConversationIds ,
} = getMessageRecipients ( {
message ,
conversation ,
} ) ;
2021-08-31 20:58:39 +00:00
2021-09-07 20:39:14 +00:00
if ( untrustedConversationIds . length ) {
2021-08-31 20:58:39 +00:00
log . info (
2021-09-07 20:39:14 +00:00
` message ${ messageId } sending blocked because ${ untrustedConversationIds . length } conversation(s) were untrusted. Giving up on the job, but it may be reborn later `
) ;
window . reduxActions . conversations . messageStoppedByMissingVerification (
messageId ,
untrustedConversationIds
2021-08-31 20:58:39 +00:00
) ;
2022-01-04 22:24:18 +00:00
await markMessageFailed ( message , messageSendErrors ) ;
2021-08-31 20:58:39 +00:00
return ;
}
2021-09-07 20:39:14 +00:00
if ( ! allRecipientIdentifiers . length ) {
log . warn (
` trying to send message ${ messageId } but it looks like it was already sent to everyone. This is unexpected, but we're giving up `
) ;
return ;
}
2021-08-31 20:58:39 +00:00
2021-09-07 20:39:14 +00:00
const {
attachments ,
body ,
deletedForEveryoneTimestamp ,
expireTimer ,
mentions ,
messageTimestamp ,
preview ,
profileKey ,
quote ,
sticker ,
2021-10-13 18:50:58 +00:00
} = await getMessageSendData ( { conversation , log , message } ) ;
2021-09-07 20:39:14 +00:00
let messageSendPromise : Promise < unknown > ;
if ( recipientIdentifiersWithoutMe . length === 0 ) {
log . info ( 'sending sync message only' ) ;
const dataMessage = await window . textsecure . messaging . getDataMessage ( {
2021-08-31 20:58:39 +00:00
attachments ,
body ,
2021-10-29 23:19:44 +00:00
groupV2 : conversation.getGroupV2Info ( {
members : recipientIdentifiersWithoutMe ,
} ) ,
2021-08-31 20:58:39 +00:00
deletedForEveryoneTimestamp ,
expireTimer ,
preview ,
profileKey ,
quote ,
2021-09-07 20:39:14 +00:00
recipients : allRecipientIdentifiers ,
2021-08-31 20:58:39 +00:00
sticker ,
2021-09-07 20:39:14 +00:00
timestamp : messageTimestamp ,
} ) ;
messageSendPromise = message . sendSyncMessageOnly (
dataMessage ,
saveErrors
) ;
} else {
const conversationType = conversation . get ( 'type' ) ;
const sendOptions = await getSendOptions ( conversation . attributes ) ;
const { ContentHint } = Proto . UnidentifiedSenderMessage . Message ;
let innerPromise : Promise < CallbackResultType > ;
if ( conversationType === Message . GROUP ) {
log . info ( 'sending group message' ) ;
2021-09-27 18:29:06 +00:00
innerPromise = conversation . queueJob (
'normalMessageSendJobQueue' ,
( ) = >
window . Signal . Util . sendToGroup ( {
2021-12-10 02:15:59 +00:00
contentHint : ContentHint.RESENDABLE ,
2021-09-27 18:29:06 +00:00
groupSendOptions : {
attachments ,
deletedForEveryoneTimestamp ,
expireTimer ,
2021-10-29 23:19:44 +00:00
groupV1 : conversation.getGroupV1Info (
2021-09-27 18:29:06 +00:00
recipientIdentifiersWithoutMe
) ,
2021-10-29 23:19:44 +00:00
groupV2 : conversation.getGroupV2Info ( {
members : recipientIdentifiersWithoutMe ,
} ) ,
2021-09-27 18:29:06 +00:00
messageText : body ,
preview ,
profileKey ,
quote ,
sticker ,
timestamp : messageTimestamp ,
mentions ,
} ,
messageId ,
sendOptions ,
2021-12-10 02:15:59 +00:00
sendTarget : conversation.toSenderKeyTarget ( ) ,
2021-09-27 18:29:06 +00:00
sendType : 'message' ,
} )
) ;
2021-09-07 20:39:14 +00:00
} else {
log . info ( 'sending direct message' ) ;
innerPromise = window . textsecure . messaging . sendMessageToIdentifier ( {
identifier : recipientIdentifiersWithoutMe [ 0 ] ,
messageText : body ,
2021-08-31 20:58:39 +00:00
attachments ,
quote ,
2021-09-07 20:39:14 +00:00
preview ,
2021-08-31 20:58:39 +00:00
sticker ,
2021-10-05 22:10:08 +00:00
reaction : undefined ,
2021-09-07 20:39:14 +00:00
deletedForEveryoneTimestamp ,
2021-08-31 20:58:39 +00:00
timestamp : messageTimestamp ,
2021-09-07 20:39:14 +00:00
expireTimer ,
contentHint : ContentHint.RESENDABLE ,
groupId : undefined ,
profileKey ,
options : sendOptions ,
2021-08-31 20:58:39 +00:00
} ) ;
}
2021-09-07 20:39:14 +00:00
messageSendPromise = message . send (
handleMessageSend ( innerPromise , {
messageIds : [ messageId ] ,
sendType : 'message' ,
} ) ,
saveErrors
2021-08-31 20:58:39 +00:00
) ;
2021-09-07 20:39:14 +00:00
}
2021-08-31 20:58:39 +00:00
2021-09-07 20:39:14 +00:00
await messageSendPromise ;
2021-08-31 20:58:39 +00:00
2021-09-07 20:39:14 +00:00
if (
getLastChallengeError ( {
errors : messageSendErrors ,
} )
) {
log . info (
` message ${ messageId } hit a spam challenge. Not retrying any more `
) ;
await message . saveErrors ( messageSendErrors ) ;
return ;
}
2021-08-31 20:58:39 +00:00
2021-09-07 20:39:14 +00:00
const didFullySend =
! messageSendErrors . length || didSendToEveryone ( message ) ;
if ( ! didFullySend ) {
throw new Error ( 'message did not fully send' ) ;
}
2022-01-12 00:50:11 +00:00
} catch ( thrownError : unknown ) {
await handleMultipleSendErrors ( {
errors : [ thrownError , . . . messageSendErrors ] ,
isFinalAttempt ,
log ,
markFailed : ( ) = > markMessageFailed ( message , messageSendErrors ) ,
timeRemaining ,
2021-09-10 20:13:45 +00:00
} ) ;
2021-09-07 20:39:14 +00:00
}
2021-08-31 20:58:39 +00:00
}
}
export const normalMessageSendJobQueue = new NormalMessageSendJobQueue ( {
store : jobQueueDatabaseStore ,
queueType : 'normal message send' ,
maxAttempts : MAX_ATTEMPTS ,
} ) ;
function getMessageRecipients ( {
conversation ,
message ,
} : Readonly < {
conversation : ConversationModel ;
message : MessageModel ;
} > ) : {
allRecipientIdentifiers : Array < string > ;
recipientIdentifiersWithoutMe : Array < string > ;
untrustedConversationIds : Array < string > ;
} {
const allRecipientIdentifiers : Array < string > = [ ] ;
const recipientIdentifiersWithoutMe : Array < string > = [ ] ;
const untrustedConversationIds : Array < string > = [ ] ;
2021-11-11 22:43:05 +00:00
const currentConversationRecipients =
conversation . getRecipientConversationIds ( ) ;
2021-08-31 20:58:39 +00:00
Object . entries ( message . get ( 'sendStateByConversationId' ) || { } ) . forEach (
( [ recipientConversationId , sendState ] ) = > {
if ( isSent ( sendState . status ) ) {
return ;
}
const recipient = window . ConversationController . get (
recipientConversationId
) ;
if ( ! recipient ) {
return ;
}
const isRecipientMe = isMe ( recipient . attributes ) ;
if (
! currentConversationRecipients . has ( recipientConversationId ) &&
! isRecipientMe
) {
return ;
}
if ( recipient . isUntrusted ( ) ) {
untrustedConversationIds . push ( recipientConversationId ) ;
}
const recipientIdentifier = recipient . getSendTarget ( ) ;
if ( ! recipientIdentifier ) {
return ;
}
allRecipientIdentifiers . push ( recipientIdentifier ) ;
if ( ! isRecipientMe ) {
recipientIdentifiersWithoutMe . push ( recipientIdentifier ) ;
}
}
) ;
return {
allRecipientIdentifiers ,
recipientIdentifiersWithoutMe ,
untrustedConversationIds ,
} ;
}
async function getMessageSendData ( {
conversation ,
2021-10-13 18:50:58 +00:00
log ,
2021-08-31 20:58:39 +00:00
message ,
} : Readonly < {
conversation : ConversationModel ;
2021-10-13 18:50:58 +00:00
log : LoggerType ;
2021-08-31 20:58:39 +00:00
message : MessageModel ;
} > ) : Promise < {
attachments : Array < AttachmentType > ;
body : undefined | string ;
deletedForEveryoneTimestamp : undefined | number ;
expireTimer : undefined | number ;
mentions : undefined | BodyRangesType ;
messageTimestamp : number ;
2021-10-05 22:10:08 +00:00
preview : Array < LinkPreviewType > ;
2021-09-24 00:49:05 +00:00
profileKey : undefined | Uint8Array ;
2021-08-31 20:58:39 +00:00
quote : WhatIsThis ;
sticker : WhatIsThis ;
} > {
2021-10-13 18:50:58 +00:00
let messageTimestamp : number ;
const sentAt = message . get ( 'sent_at' ) ;
const timestamp = message . get ( 'timestamp' ) ;
if ( sentAt ) {
messageTimestamp = sentAt ;
} else if ( timestamp ) {
log . error ( 'message lacked sent_at. Falling back to timestamp' ) ;
messageTimestamp = timestamp ;
} else {
log . error (
'message lacked sent_at and timestamp. Falling back to current time'
) ;
messageTimestamp = Date . now ( ) ;
}
2021-08-31 20:58:39 +00:00
2021-11-11 22:43:05 +00:00
const [ attachmentsWithData , preview , quote , sticker , profileKey ] =
await Promise . all ( [
// We don't update the caches here because (1) we expect the caches to be populated
// on initial send, so they should be there in the 99% case (2) if you're retrying
// a failed message across restarts, we don't touch the cache for simplicity. If
// sends are failing, let's not add the complication of a cache.
Promise . all ( ( message . get ( 'attachments' ) ? ? [ ] ) . map ( loadAttachmentData ) ) ,
message . cachedOutgoingPreviewData ||
loadPreviewData ( message . get ( 'preview' ) ) ,
message . cachedOutgoingQuoteData || loadQuoteData ( message . get ( 'quote' ) ) ,
message . cachedOutgoingStickerData ||
loadStickerData ( message . get ( 'sticker' ) ) ,
conversation . get ( 'profileSharing' )
? ourProfileKeyService . get ( )
: undefined ,
] ) ;
2021-08-31 20:58:39 +00:00
const { body , attachments } = window . Whisper . Message . getLongMessageAttachment (
{
body : message.get ( 'body' ) ,
attachments : attachmentsWithData ,
now : messageTimestamp ,
}
) ;
return {
attachments ,
body ,
deletedForEveryoneTimestamp : message.get ( 'deletedForEveryoneTimestamp' ) ,
2021-09-07 16:36:19 +00:00
expireTimer : message.get ( 'expireTimer' ) ,
2021-08-31 20:58:39 +00:00
mentions : message.get ( 'bodyRanges' ) ,
messageTimestamp ,
preview ,
profileKey ,
quote ,
sticker ,
} ;
}
async function markMessageFailed (
message : MessageModel ,
errors : Array < Error >
) : Promise < void > {
message . markFailed ( ) ;
message . saveErrors ( errors , { skipSave : true } ) ;
2021-12-20 21:04:02 +00:00
await window . Signal . Data . saveMessage ( message . attributes , {
ourUuid : window.textsecure.storage.user.getCheckedUuid ( ) . toString ( ) ,
} ) ;
2021-08-31 20:58:39 +00:00
}
function didSendToEveryone ( message : Readonly < MessageModel > ) : boolean {
const sendStateByConversationId =
message . get ( 'sendStateByConversationId' ) || { } ;
return Object . values ( sendStateByConversationId ) . every ( sendState = >
isSent ( sendState . status )
) ;
}