2022-08-02 19:31:55 +00:00
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isEqual } from 'lodash' ;
2022-08-04 19:23:24 +00:00
import type {
AttachmentWithHydratedData ,
TextAttachmentType ,
} from '../../types/Attachment' ;
2022-08-02 19:31:55 +00:00
import type { ConversationModel } from '../../models/conversations' ;
import type {
ConversationQueueJobBundle ,
StoryJobData ,
} from '../conversationJobQueue' ;
import type { LoggerType } from '../../types/Logging' ;
import type { MessageModel } from '../../models/messages' ;
import type { SenderKeyInfoType } from '../../model-types.d' ;
import type {
SendState ,
SendStateByConversationId ,
} from '../../messages/MessageSendState' ;
2022-11-11 04:10:30 +00:00
import {
isSent ,
SendActionType ,
sendStateReducer ,
} from '../../messages/MessageSendState' ;
2022-08-02 19:31:55 +00:00
import type { UUIDStringType } from '../../types/UUID' ;
import * as Errors from '../../types/errors' ;
import dataInterface from '../../sql/Client' ;
import { SignalService as Proto } from '../../protobuf' ;
import { getMessageById } from '../../messages/getMessageById' ;
import {
getSendOptions ,
getSendOptionsForRecipients ,
} from '../../util/getSendOptions' ;
import { handleMessageSend } from '../../util/handleMessageSend' ;
import { handleMultipleSendErrors } from './handleMultipleSendErrors' ;
2022-08-09 03:26:21 +00:00
import { isGroupV2 , isMe } from '../../util/whatTypeOfConversation' ;
2022-08-02 19:31:55 +00:00
import { isNotNil } from '../../util/isNotNil' ;
import { ourProfileKeyService } from '../../services/ourProfileKey' ;
import { sendContentMessageToGroup } from '../../util/sendToGroup' ;
2022-10-15 00:22:04 +00:00
import { SendMessageChallengeError } from '../../textsecure/Errors' ;
2022-08-02 19:31:55 +00:00
export async function sendStory (
conversation : ConversationModel ,
{
isFinalAttempt ,
messaging ,
shouldContinue ,
timeRemaining ,
log ,
} : ConversationQueueJobBundle ,
data : StoryJobData
) : Promise < void > {
2022-08-04 19:23:24 +00:00
const { messageIds , timestamp } = data ;
2022-08-02 19:31:55 +00:00
const profileKey = await ourProfileKeyService . get ( ) ;
if ( ! profileKey ) {
log . info ( 'stories.sendStory: no profile key cannot send' ) ;
return ;
}
2022-10-15 00:22:04 +00:00
// We can send a story to either:
// 1) the current group, or
// 2) all selected distribution lists (in queue for our own conversationId)
if ( ! isGroupV2 ( conversation . attributes ) && ! isMe ( conversation . attributes ) ) {
log . error (
'stories.sendStory: Conversation is neither groupV2 nor our own. Cannot send.'
) ;
return ;
}
2022-08-04 19:23:24 +00:00
// We want to generate the StoryMessage proto once at the top level so we
// can reuse it but first we'll need textAttachment | fileAttachment.
// This function pulls off the attachment and generates the proto from the
// first message on the list prior to continuing.
const originalStoryMessage = await ( async ( ) : Promise <
Proto . StoryMessage | undefined
> = > {
const [ messageId ] = messageIds ;
const message = await getMessageById ( messageId ) ;
if ( ! message ) {
log . info (
2022-08-09 03:26:21 +00:00
` stories.sendStory( ${ messageId } ): message was not found, maybe because it was deleted. Giving up on sending it `
2022-08-04 19:23:24 +00:00
) ;
return ;
}
2022-08-10 18:37:19 +00:00
const messageConversation = message . getConversation ( ) ;
if ( messageConversation !== conversation ) {
log . error (
2022-10-22 00:38:49 +00:00
` stories.sendStory( ${ timestamp } ): Message conversation ' ${ messageConversation ? . idForLogging ( ) } ' does not match job conversation ${ conversation . idForLogging ( ) } `
2022-08-10 18:37:19 +00:00
) ;
return ;
}
2022-08-04 19:23:24 +00:00
const attachments = message . get ( 'attachments' ) || [ ] ;
const [ attachment ] = attachments ;
if ( ! attachment ) {
log . info (
2022-10-22 00:38:49 +00:00
` stories.sendStory( ${ timestamp } ): message does not have any attachments to send. Giving up on sending it `
2022-08-04 19:23:24 +00:00
) ;
return ;
}
let textAttachment : TextAttachmentType | undefined ;
let fileAttachment : AttachmentWithHydratedData | undefined ;
if ( attachment . textAttachment ) {
textAttachment = attachment . textAttachment ;
} else {
fileAttachment = await window . Signal . Migrations . loadAttachmentData (
attachment
) ;
}
2022-08-10 18:37:19 +00:00
const groupV2 = isGroupV2 ( conversation . attributes )
? conversation . getGroupV2Info ( )
: undefined ;
2022-08-04 19:23:24 +00:00
// Some distribution lists need allowsReplies false, some need it set to true
// we create this proto (for the sync message) and also to re-use some of the
// attributes inside it.
return messaging . getStoryMessage ( {
allowsReplies : true ,
fileAttachment ,
2022-08-10 18:37:19 +00:00
groupV2 ,
2022-08-04 19:23:24 +00:00
textAttachment ,
profileKey ,
} ) ;
} ) ( ) ;
if ( ! originalStoryMessage ) {
return ;
}
2022-08-02 19:31:55 +00:00
const canReplyUuids = new Set < string > ( ) ;
const recipientsByUuid = new Map < string , Set < string > > ( ) ;
2022-08-09 03:26:21 +00:00
const sentConversationIds = new Map < string , SendState > ( ) ;
const sentUuids = new Set < string > ( ) ;
2022-08-02 19:31:55 +00:00
// This function is used to keep track of all the recipients so once we're
// done with our send we can build up the storyMessageRecipients object for
// sending in the sync message.
2022-08-09 03:26:21 +00:00
function addDistributionListToUuidSent (
listId : string | undefined ,
2022-08-02 19:31:55 +00:00
uuid : string ,
canReply? : boolean
) : void {
if ( conversation . get ( 'uuid' ) === uuid ) {
return ;
}
const distributionListIds = recipientsByUuid . get ( uuid ) || new Set < string > ( ) ;
2022-08-09 03:26:21 +00:00
if ( listId ) {
recipientsByUuid . set ( uuid , new Set ( [ . . . distributionListIds , listId ] ) ) ;
} else {
recipientsByUuid . set ( uuid , distributionListIds ) ;
}
2022-08-02 19:31:55 +00:00
if ( canReply ) {
canReplyUuids . add ( uuid ) ;
}
}
let isSyncMessageUpdate = false ;
2022-10-15 00:22:04 +00:00
// Note: We capture errors here so we are sure to wait for every send process to
// complete, and so we can send a sync message afterwards if we sent the story
// successfully to at least one recipient.
const sendResults = await Promise . allSettled (
messageIds . map ( async ( messageId : string ) : Promise < void > = > {
2022-08-02 19:31:55 +00:00
const message = await getMessageById ( messageId ) ;
if ( ! message ) {
log . info (
2022-08-09 03:26:21 +00:00
` stories.sendStory( ${ messageId } ): message was not found, maybe because it was deleted. Giving up on sending it `
2022-08-02 19:31:55 +00:00
) ;
return ;
}
2022-11-11 04:10:30 +00:00
const distributionId = message . get ( 'storyDistributionListId' ) ;
const logId = ` stories.sendStory( ${ timestamp } / ${ distributionId } ) ` ;
2022-10-22 00:38:49 +00:00
if ( message . get ( 'timestamp' ) !== timestamp ) {
log . error (
2022-11-11 04:10:30 +00:00
` ${ logId } : Message timestamp ${ message . get (
2022-10-22 00:38:49 +00:00
'timestamp'
) } does not match job timestamp `
) ;
return ;
}
2022-08-02 19:31:55 +00:00
const messageConversation = message . getConversation ( ) ;
if ( messageConversation !== conversation ) {
log . error (
2022-11-11 04:10:30 +00:00
` ${ logId } : Message conversation ' ${ messageConversation ? . idForLogging ( ) } ' does not match job conversation ${ conversation . idForLogging ( ) } `
2022-08-02 19:31:55 +00:00
) ;
return ;
}
if ( message . isErased ( ) || message . get ( 'deletedForEveryone' ) ) {
2022-11-11 04:10:30 +00:00
log . info ( ` ${ logId } : message was erased. Giving up on sending it ` ) ;
2022-08-02 19:31:55 +00:00
return ;
}
const listId = message . get ( 'storyDistributionListId' ) ;
2022-08-09 03:26:21 +00:00
const receiverId = isGroupV2 ( messageConversation . attributes )
? messageConversation . id
: listId ;
2022-08-02 19:31:55 +00:00
2022-08-09 03:26:21 +00:00
if ( ! receiverId ) {
2022-08-02 19:31:55 +00:00
log . info (
2022-11-11 04:10:30 +00:00
` ${ logId } : did not get a valid recipient ID for message. Giving up on sending it `
2022-08-02 19:31:55 +00:00
) ;
return ;
}
2022-08-09 03:26:21 +00:00
const distributionList = isGroupV2 ( messageConversation . attributes )
? undefined
: await dataInterface . getStoryDistributionWithMembers ( receiverId ) ;
2022-08-02 19:31:55 +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 ) {
2022-11-11 04:10:30 +00:00
log . info ( ` ${ logId } : ran out of time. Giving up on sending it ` ) ;
2022-08-02 19:31:55 +00:00
await markMessageFailed ( message , [
new Error ( 'Message send ran out of time' ) ,
] ) ;
return ;
}
let originalError : Error | undefined ;
const {
2022-08-09 03:26:21 +00:00
allRecipientIds ,
2022-08-02 19:31:55 +00:00
allowedReplyByUuid ,
2022-08-09 03:26:21 +00:00
pendingSendRecipientIds ,
sentRecipientIds ,
2022-08-02 19:31:55 +00:00
untrustedUuids ,
} = getMessageRecipients ( {
log ,
message ,
} ) ;
try {
if ( untrustedUuids . length ) {
window . reduxActions . conversations . conversationStoppedByMissingVerification (
{
conversationId : conversation.id ,
2022-11-11 04:10:30 +00:00
distributionId ,
2022-08-02 19:31:55 +00:00
untrustedUuids ,
}
) ;
throw new Error (
2022-11-11 04:10:30 +00:00
` ${ logId } : sending blocked because ${ untrustedUuids . length } conversation(s) were untrusted. Failing this attempt. `
2022-08-02 19:31:55 +00:00
) ;
}
2022-08-09 03:26:21 +00:00
if ( ! pendingSendRecipientIds . length ) {
allRecipientIds . forEach ( uuid = >
addDistributionListToUuidSent (
2022-08-02 19:31:55 +00:00
listId ,
uuid ,
allowedReplyByUuid . get ( uuid )
)
) ;
return ;
}
const { ContentHint } = Proto . UnidentifiedSenderMessage . Message ;
2022-08-09 03:26:21 +00:00
const recipientsSet = new Set ( pendingSendRecipientIds ) ;
2022-08-02 19:31:55 +00:00
const sendOptions = await getSendOptionsForRecipients (
2022-10-07 17:02:08 +00:00
pendingSendRecipientIds ,
{ story : true }
2022-08-02 19:31:55 +00:00
) ;
log . info (
2022-10-22 00:38:49 +00:00
` stories.sendStory( ${ timestamp } ): sending story to ${ receiverId } `
2022-08-02 19:31:55 +00:00
) ;
const storyMessage = new Proto . StoryMessage ( ) ;
storyMessage . profileKey = originalStoryMessage . profileKey ;
storyMessage . fileAttachment = originalStoryMessage . fileAttachment ;
storyMessage . textAttachment = originalStoryMessage . textAttachment ;
storyMessage . group = originalStoryMessage . group ;
2022-08-09 03:26:21 +00:00
storyMessage . allowsReplies =
isGroupV2 ( messageConversation . attributes ) ||
Boolean ( distributionList ? . allowsReplies ) ;
2022-09-08 23:17:38 +00:00
let inMemorySenderKeyInfo = distributionList ? . senderKeyInfo ;
2022-08-09 03:26:21 +00:00
const sendTarget = distributionList
? {
getGroupId : ( ) = > undefined ,
getMembers : ( ) = >
pendingSendRecipientIds
. map ( uuid = > window . ConversationController . get ( uuid ) )
. filter ( isNotNil ) ,
hasMember : ( uuid : UUIDStringType ) = > recipientsSet . has ( uuid ) ,
idForLogging : ( ) = > ` dl( ${ receiverId } ) ` ,
isGroupV2 : ( ) = > true ,
isValid : ( ) = > true ,
2022-09-08 23:17:38 +00:00
getSenderKeyInfo : ( ) = > inMemorySenderKeyInfo ,
saveSenderKeyInfo : async ( senderKeyInfo : SenderKeyInfoType ) = > {
inMemorySenderKeyInfo = senderKeyInfo ;
await dataInterface . modifyStoryDistribution ( {
2022-08-09 03:26:21 +00:00
. . . distributionList ,
senderKeyInfo ,
2022-09-08 23:17:38 +00:00
} ) ;
} ,
2022-08-09 03:26:21 +00:00
}
: conversation . toSenderKeyTarget ( ) ;
2022-08-02 19:31:55 +00:00
const contentMessage = new Proto . Content ( ) ;
contentMessage . storyMessage = storyMessage ;
const innerPromise = sendContentMessageToGroup ( {
contentHint : ContentHint.IMPLICIT ,
contentMessage ,
isPartialSend : false ,
messageId : undefined ,
2022-08-09 03:26:21 +00:00
recipients : pendingSendRecipientIds ,
2022-08-02 19:31:55 +00:00
sendOptions ,
2022-08-09 03:26:21 +00:00
sendTarget ,
2022-08-02 19:31:55 +00:00
sendType : 'story' ,
2022-09-30 16:59:36 +00:00
story : true ,
2022-08-19 21:12:05 +00:00
timestamp : message.get ( 'timestamp' ) ,
2022-08-02 19:31:55 +00:00
urgent : false ,
} ) ;
2022-10-15 00:22:04 +00:00
// Don't send normal sync messages; a story sync is sent at the end of the process
message . doNotSendSyncMessage = true ;
2022-08-02 19:31:55 +00:00
const messageSendPromise = message . send (
handleMessageSend ( innerPromise , {
messageIds : [ messageId ] ,
sendType : 'story' ,
} ) ,
saveErrors
) ;
// Because message.send swallows and processes errors, we'll await the
// inner promise to get the SendMessageProtoError, which gives us
// information upstream processors need to detect certain kinds of situations.
try {
await innerPromise ;
} catch ( error ) {
if ( error instanceof Error ) {
originalError = error ;
} else {
log . error (
2022-11-11 04:10:30 +00:00
` ${ logId } : promiseForError threw something other than an error: ${ Errors . toLogFormat (
2022-08-02 19:31:55 +00:00
error
) } `
) ;
}
}
await messageSendPromise ;
// Track sendState across message sends so that we can update all
// subsequent messages.
const sendStateByConversationId =
message . get ( 'sendStateByConversationId' ) || { } ;
Object . entries ( sendStateByConversationId ) . forEach (
( [ recipientConversationId , sendState ] ) = > {
2022-08-09 03:26:21 +00:00
if ( ! isSent ( sendState . status ) ) {
2022-08-02 19:31:55 +00:00
return ;
}
2022-08-09 03:26:21 +00:00
sentConversationIds . set ( recipientConversationId , sendState ) ;
const recipient = window . ConversationController . get (
recipientConversationId
2022-08-02 19:31:55 +00:00
) ;
2022-08-09 03:26:21 +00:00
const uuid = recipient ? . get ( 'uuid' ) ;
if ( ! uuid ) {
return ;
}
sentUuids . add ( uuid ) ;
2022-08-02 19:31:55 +00:00
}
) ;
2022-08-09 03:26:21 +00:00
allRecipientIds . forEach ( uuid = > {
addDistributionListToUuidSent (
listId ,
uuid ,
allowedReplyByUuid . get ( uuid )
) ;
} ) ;
2022-08-02 19:31:55 +00:00
const didFullySend =
! messageSendErrors . length || didSendToEveryone ( message ) ;
if ( ! didFullySend ) {
2022-11-11 04:10:30 +00:00
throw new Error ( ` ${ logId } : message did not fully send ` ) ;
2022-08-02 19:31:55 +00:00
}
} catch ( thrownError : unknown ) {
const errors = [ thrownError , . . . messageSendErrors ] ;
2022-10-15 00:22:04 +00:00
// We need to check for this here because we can only throw one error up to
// conversationJobQueue.
errors . forEach ( error = > {
if ( error instanceof SendMessageChallengeError ) {
window . Signal . challengeHandler ? . register (
{
conversationId : conversation.id ,
createdAt : Date.now ( ) ,
retryAt : error.retryAt ,
token : error.data?.token ,
reason :
'conversationJobQueue.run(' +
2022-11-11 04:10:30 +00:00
` ${ conversation . idForLogging ( ) } , story, ${ timestamp } / ${ distributionId } ) ` ,
2022-10-15 00:22:04 +00:00
} ,
error . data
) ;
}
} ) ;
2022-08-02 19:31:55 +00:00
await handleMultipleSendErrors ( {
errors ,
isFinalAttempt ,
log ,
markFailed : ( ) = > markMessageFailed ( message , messageSendErrors ) ,
timeRemaining ,
// In the case of a failed group send thrownError will not be
// SentMessageProtoError, but we should have been able to harvest
// the original error. In the Note to Self send case, thrownError
// will be the error we care about, and we won't have an originalError.
toThrow : originalError || thrownError ,
} ) ;
} finally {
2022-08-09 03:26:21 +00:00
isSyncMessageUpdate = sentRecipientIds . length > 0 ;
}
} )
) ;
// Some contacts are duplicated across lists and we don't send duplicate
// messages but we still want to make sure that the sendStateByConversationId
// is kept in sync across all messages.
await Promise . all (
messageIds . map ( async messageId = > {
const message = await getMessageById ( messageId ) ;
if ( ! message ) {
return ;
2022-08-02 19:31:55 +00:00
}
2022-08-09 03:26:21 +00:00
const oldSendStateByConversationId =
message . get ( 'sendStateByConversationId' ) || { } ;
const newSendStateByConversationId = Object . keys (
oldSendStateByConversationId
) . reduce ( ( acc , conversationId ) = > {
const sendState = sentConversationIds . get ( conversationId ) ;
if ( sendState ) {
return {
. . . acc ,
[ conversationId ] : sendState ,
} ;
}
2022-11-11 04:10:30 +00:00
const oldSendState = {
. . . oldSendStateByConversationId [ conversationId ] ,
} ;
if ( ! oldSendState ) {
return acc ;
}
const recipient = window . ConversationController . get ( conversationId ) ;
if ( ! recipient ) {
return acc ;
}
if ( recipient . isUnregistered ( ) ) {
if ( ! isSent ( oldSendState . status ) ) {
// We should have filtered this out on initial send, but we'll drop them from
// send list here if needed.
return acc ;
}
// If a previous send to them did succeed, we'll keep that status around
return {
. . . acc ,
[ conversationId ] : oldSendState ,
} ;
}
return {
. . . acc ,
[ conversationId ] : sendStateReducer ( oldSendState , {
type : SendActionType . Failed ,
updatedAt : Date.now ( ) ,
} ) ,
} ;
2022-08-09 03:26:21 +00:00
} , { } as SendStateByConversationId ) ;
if ( isEqual ( oldSendStateByConversationId , newSendStateByConversationId ) ) {
return ;
}
message . set ( 'sendStateByConversationId' , newSendStateByConversationId ) ;
return window . Signal . Data . saveMessage ( message . attributes , {
ourUuid : window.textsecure.storage.user.getCheckedUuid ( ) . toString ( ) ,
} ) ;
2022-08-02 19:31:55 +00:00
} )
) ;
2022-08-09 03:26:21 +00:00
// Remove any unsent recipients
recipientsByUuid . forEach ( ( _value , uuid ) = > {
if ( sentUuids . has ( uuid ) ) {
return ;
}
recipientsByUuid . delete ( uuid ) ;
} ) ;
// Build up the sync message's storyMessageRecipients and send it
2022-08-02 19:31:55 +00:00
const storyMessageRecipients : Array < {
destinationUuid : string ;
distributionListIds : Array < string > ;
isAllowedToReply : boolean ;
} > = [ ] ;
recipientsByUuid . forEach ( ( distributionListIds , destinationUuid ) = > {
storyMessageRecipients . push ( {
destinationUuid ,
distributionListIds : Array.from ( distributionListIds ) ,
isAllowedToReply : canReplyUuids.has ( destinationUuid ) ,
} ) ;
} ) ;
2022-10-15 00:22:04 +00:00
if ( storyMessageRecipients . length === 0 ) {
log . warn (
'No successful sends; will not send a sync message for this attempt'
) ;
} else {
const options = await getSendOptions ( conversation . attributes , {
syncMessage : true ,
} ) ;
2022-08-02 19:31:55 +00:00
2022-10-15 00:22:04 +00:00
await messaging . sendSyncMessage ( {
// Note: these two fields will be undefined if we're sending to a group
destination : conversation.get ( 'e164' ) ,
destinationUuid : conversation.get ( 'uuid' ) ,
storyMessage : originalStoryMessage ,
storyMessageRecipients ,
expirationStartTimestamp : null ,
isUpdate : isSyncMessageUpdate ,
options ,
timestamp ,
urgent : false ,
} ) ;
}
// We can only throw one Error up to conversationJobQueue to fail the send
const sendErrors : Array < PromiseRejectedResult > = [ ] ;
sendResults . forEach ( result = > {
if ( result . status === 'rejected' ) {
sendErrors . push ( result ) ;
}
2022-08-02 19:31:55 +00:00
} ) ;
2022-10-15 00:22:04 +00:00
if ( sendErrors . length ) {
throw sendErrors [ 0 ] . reason ;
}
2022-08-02 19:31:55 +00:00
}
function getMessageRecipients ( {
log ,
message ,
} : Readonly < {
log : LoggerType ;
message : MessageModel ;
} > ) : {
2022-08-09 03:26:21 +00:00
allRecipientIds : Array < string > ;
2022-08-02 19:31:55 +00:00
allowedReplyByUuid : Map < string , boolean > ;
2022-08-09 03:26:21 +00:00
pendingSendRecipientIds : Array < string > ;
sentRecipientIds : Array < string > ;
2022-11-11 04:10:30 +00:00
untrustedUuids : Array < UUIDStringType > ;
2022-08-02 19:31:55 +00:00
} {
2022-08-09 03:26:21 +00:00
const allRecipientIds : Array < string > = [ ] ;
2022-08-02 19:31:55 +00:00
const allowedReplyByUuid = new Map < string , boolean > ( ) ;
2022-08-09 03:26:21 +00:00
const pendingSendRecipientIds : Array < string > = [ ] ;
const sentRecipientIds : Array < string > = [ ] ;
2022-11-11 04:10:30 +00:00
const untrustedUuids : Array < UUIDStringType > = [ ] ;
2022-08-02 19:31:55 +00:00
Object . entries ( message . get ( 'sendStateByConversationId' ) || { } ) . forEach (
( [ recipientConversationId , sendState ] ) = > {
const recipient = window . ConversationController . get (
recipientConversationId
) ;
if ( ! recipient ) {
return ;
}
const isRecipientMe = isMe ( recipient . attributes ) ;
2022-08-09 03:26:21 +00:00
if ( isRecipientMe ) {
return ;
}
2022-08-02 19:31:55 +00:00
if ( recipient . isUntrusted ( ) ) {
const uuid = recipient . get ( 'uuid' ) ;
if ( ! uuid ) {
log . error (
` stories.sendStory/getMessageRecipients: Untrusted conversation ${ recipient . idForLogging ( ) } missing UUID. `
) ;
return ;
}
untrustedUuids . push ( uuid ) ;
return ;
}
if ( recipient . isUnregistered ( ) ) {
return ;
}
2022-08-09 03:26:21 +00:00
const recipientSendTarget = recipient . getSendTarget ( ) ;
if ( ! recipientSendTarget ) {
2022-08-02 19:31:55 +00:00
return ;
}
allowedReplyByUuid . set (
2022-08-09 03:26:21 +00:00
recipientSendTarget ,
2022-08-02 19:31:55 +00:00
Boolean ( sendState . isAllowedToReplyToStory )
) ;
2022-08-09 03:26:21 +00:00
allRecipientIds . push ( recipientSendTarget ) ;
2022-08-02 19:31:55 +00:00
2022-08-09 03:26:21 +00:00
if ( sendState . isAlreadyIncludedInAnotherDistributionList ) {
2022-08-02 19:31:55 +00:00
return ;
}
2022-08-09 03:26:21 +00:00
if ( isSent ( sendState . status ) ) {
sentRecipientIds . push ( recipientSendTarget ) ;
return ;
2022-08-02 19:31:55 +00:00
}
2022-08-09 03:26:21 +00:00
pendingSendRecipientIds . push ( recipientSendTarget ) ;
2022-08-02 19:31:55 +00:00
}
) ;
return {
2022-08-09 03:26:21 +00:00
allRecipientIds ,
2022-08-02 19:31:55 +00:00
allowedReplyByUuid ,
2022-08-09 03:26:21 +00:00
pendingSendRecipientIds ,
sentRecipientIds ,
2022-08-02 19:31:55 +00:00
untrustedUuids ,
} ;
}
async function markMessageFailed (
message : MessageModel ,
errors : Array < Error >
) : Promise < void > {
message . markFailed ( ) ;
message . saveErrors ( errors , { skipSave : true } ) ;
await window . Signal . Data . saveMessage ( message . attributes , {
ourUuid : window.textsecure.storage.user.getCheckedUuid ( ) . toString ( ) ,
} ) ;
}
function didSendToEveryone ( message : Readonly < MessageModel > ) : boolean {
const sendStateByConversationId =
message . get ( 'sendStateByConversationId' ) || { } ;
2022-08-09 03:26:21 +00:00
return Object . values ( sendStateByConversationId ) . every (
sendState = >
sendState . isAlreadyIncludedInAnotherDistributionList ||
isSent ( sendState . status )
2022-08-02 19:31:55 +00:00
) ;
}