// // Copyright 2017 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // #import "TSOutgoingMessage.h" #import "AppReadiness.h" #import "OWSOutgoingSyncMessage.h" #import "ProtoUtils.h" #import "TSAttachmentStream.h" #import "TSContactThread.h" #import "TSGroupThread.h" #import "TSQuotedMessage.h" #import #import #import NS_ASSUME_NONNULL_BEGIN const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRecipientAll"; NSString *NSStringForOutgoingMessageState(TSOutgoingMessageState value) { switch (value) { case TSOutgoingMessageStateSending: return @"TSOutgoingMessageStateSending"; case TSOutgoingMessageStateFailed: return @"TSOutgoingMessageStateFailed"; case TSOutgoingMessageStateSent_OBSOLETE: return @"TSOutgoingMessageStateSent_OBSOLETE"; case TSOutgoingMessageStateDelivered_OBSOLETE: return @"TSOutgoingMessageStateDelivered_OBSOLETE"; case TSOutgoingMessageStateSent: return @"TSOutgoingMessageStateSent"; case TSOutgoingMessageStatePending: return @"TSOutgoingMessageStatePending"; } } NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientState value) { switch (value) { case OWSOutgoingMessageRecipientStateFailed: return @"OWSOutgoingMessageRecipientStateFailed"; case OWSOutgoingMessageRecipientStateSending: return @"OWSOutgoingMessageRecipientStateSending"; case OWSOutgoingMessageRecipientStateSkipped: return @"OWSOutgoingMessageRecipientStateSkipped"; case OWSOutgoingMessageRecipientStateSent: return @"OWSOutgoingMessageRecipientStateSent"; case OWSOutgoingMessageRecipientStatePending: return @"OWSOutgoingMessageRecipientStatePending"; } } #pragma mark - @interface TSMessage (Private) - (void)removeAllAttachmentsWithTransaction:(SDSAnyWriteTransaction *)transaction; @end #pragma mark - @interface TSOutgoingMessageRecipientState () @end #pragma mark - @implementation TSOutgoingMessageRecipientState @end #pragma mark - NSUInteger const TSOutgoingMessageSchemaVersion = 1; @interface TSOutgoingMessage () @property (atomic) BOOL hasSyncedTranscript; @property (atomic, nullable) NSString *customMessage; @property (atomic, nullable) NSString *mostRecentFailureText; @property (atomic) BOOL wasNotCreatedLocally; @property (atomic) TSGroupMetaMessage groupMetaMessage; @property (nonatomic, readonly) NSUInteger outgoingMessageSchemaVersion; @property (nonatomic, readonly) TSOutgoingMessageState legacyMessageState; @property (nonatomic, readonly) BOOL legacyWasDelivered; @property (nonatomic, readonly) BOOL hasLegacyMessageState; // This property is only intended to be used by GRDB queries. @property (nonatomic, readonly) TSOutgoingMessageState storedMessageState; @end #pragma mark - @implementation TSOutgoingMessage // --- CODE GENERATION MARKER // This snippet is generated by /Scripts/sds_codegen/sds_generate.py. Do not manually edit it, instead run // `sds_codegen.sh`. // clang-format off - (instancetype)initWithGrdbId:(int64_t)grdbId uniqueId:(NSString *)uniqueId receivedAtTimestamp:(uint64_t)receivedAtTimestamp sortId:(uint64_t)sortId timestamp:(uint64_t)timestamp uniqueThreadId:(NSString *)uniqueThreadId attachmentIds:(NSArray *)attachmentIds body:(nullable NSString *)body bodyRanges:(nullable MessageBodyRanges *)bodyRanges contactShare:(nullable OWSContact *)contactShare editState:(TSEditState)editState expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds giftBadge:(nullable OWSGiftBadge *)giftBadge isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString storyReactionEmoji:(nullable NSString *)storyReactionEmoji storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage hasLegacyMessageState:(BOOL)hasLegacyMessageState hasSyncedTranscript:(BOOL)hasSyncedTranscript isVoiceMessage:(BOOL)isVoiceMessage legacyMessageState:(TSOutgoingMessageState)legacyMessageState legacyWasDelivered:(BOOL)legacyWasDelivered mostRecentFailureText:(nullable NSString *)mostRecentFailureText recipientAddressStates:(nullable NSDictionary *)recipientAddressStates storedMessageState:(TSOutgoingMessageState)storedMessageState wasNotCreatedLocally:(BOOL)wasNotCreatedLocally { self = [super initWithGrdbId:grdbId uniqueId:uniqueId receivedAtTimestamp:receivedAtTimestamp sortId:sortId timestamp:timestamp uniqueThreadId:uniqueThreadId attachmentIds:attachmentIds body:body bodyRanges:bodyRanges contactShare:contactShare editState:editState expireStartedAt:expireStartedAt expiresAt:expiresAt expiresInSeconds:expiresInSeconds giftBadge:giftBadge isGroupStoryReply:isGroupStoryReply isViewOnceComplete:isViewOnceComplete isViewOnceMessage:isViewOnceMessage linkPreview:linkPreview messageSticker:messageSticker quotedMessage:quotedMessage storedShouldStartExpireTimer:storedShouldStartExpireTimer storyAuthorUuidString:storyAuthorUuidString storyReactionEmoji:storyReactionEmoji storyTimestamp:storyTimestamp wasRemotelyDeleted:wasRemotelyDeleted]; if (!self) { return self; } _customMessage = customMessage; _groupMetaMessage = groupMetaMessage; _hasLegacyMessageState = hasLegacyMessageState; _hasSyncedTranscript = hasSyncedTranscript; _isVoiceMessage = isVoiceMessage; _legacyMessageState = legacyMessageState; _legacyWasDelivered = legacyWasDelivered; _mostRecentFailureText = mostRecentFailureText; _recipientAddressStates = recipientAddressStates; _storedMessageState = storedMessageState; _wasNotCreatedLocally = wasNotCreatedLocally; return self; } // clang-format on // --- CODE GENERATION MARKER - (nullable instancetype)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (self) { #ifndef TESTABLE_BUILD OWSAssertDebug(self.outgoingMessageSchemaVersion >= 1); #endif _outgoingMessageSchemaVersion = TSOutgoingMessageSchemaVersion; } return self; } + (instancetype)outgoingMessageInThread:(TSThread *)thread messageBody:(nullable NSString *)body { return [self outgoingMessageInThread:thread messageBody:body expiresInSeconds:0]; } + (instancetype)outgoingMessageInThread:(TSThread *)thread messageBody:(nullable NSString *)body expiresInSeconds:(uint32_t)expiresInSeconds { TSOutgoingMessageBuilder *builder = [TSOutgoingMessageBuilder outgoingMessageBuilderWithThread:thread]; builder.messageBody = body; builder.expiresInSeconds = expiresInSeconds; return [builder buildWithSneakyTransaction]; } - (instancetype)initOutgoingMessageWithBuilder:(TSOutgoingMessageBuilder *)outgoingMessageBuilder transaction:(SDSAnyReadTransaction *)transaction { self = [super initMessageWithBuilder:outgoingMessageBuilder]; if (!self) { return self; } _hasSyncedTranscript = NO; TSThread *thread = outgoingMessageBuilder.thread; TSGroupMetaMessage groupMetaMessage = outgoingMessageBuilder.groupMetaMessage; if ([thread isKindOfClass:TSGroupThread.class]) { // Unless specified, we assume group messages are "Delivery" i.e. normal messages. if (groupMetaMessage == TSGroupMetaMessageUnspecified) { _groupMetaMessage = TSGroupMetaMessageDeliver; } else { _groupMetaMessage = groupMetaMessage; } } else { OWSAssertDebug(groupMetaMessage == TSGroupMetaMessageUnspecified); // Specifying a group meta message only makes sense for Group threads _groupMetaMessage = TSGroupMetaMessageUnspecified; } _isVoiceMessage = outgoingMessageBuilder.isVoiceMessage; // New outgoing messages should immediately determine their // recipient list from current thread state. NSMutableSet *recipientAddresses = [NSMutableSet new]; if ([self isKindOfClass:[OWSOutgoingSyncMessage class]]) { // Sync messages should only be sent to linked devices. SignalServiceAddress *localAddress = [TSAccountManagerObjcBridge localAciAddressWith:transaction]; OWSAssertDebug(localAddress); [recipientAddresses addObject:localAddress]; } else { // Most messages should only be sent to the current members of the group. [recipientAddresses addObjectsFromArray:[thread recipientAddressesWithTransaction:transaction]]; // Some messages (eg certain call messages) go to a subset of the group. if (outgoingMessageBuilder.explicitRecipients != nil) { NSMutableSet *explicitRecipientAddresses = [[NSMutableSet alloc] init]; for (AciObjC *recipientAci in outgoingMessageBuilder.explicitRecipients) { [explicitRecipientAddresses addObject:[[SignalServiceAddress alloc] initWithServiceIdObjC:recipientAci]]; } [recipientAddresses intersectSet:explicitRecipientAddresses]; } // Group updates should also be sent to pending members of the group. if (outgoingMessageBuilder.additionalRecipients) { [recipientAddresses addObjectsFromArray:outgoingMessageBuilder.additionalRecipients]; } } NSMutableDictionary *recipientAddressStates = [NSMutableDictionary new]; for (SignalServiceAddress *recipientAddress in recipientAddresses) { if (!recipientAddress.isValid) { OWSFailDebug(@"Ignoring invalid address."); continue; } TSOutgoingMessageRecipientState *recipientState = [TSOutgoingMessageRecipientState new]; recipientState.state = [outgoingMessageBuilder.skippedRecipients containsObject:recipientAddress] ? OWSOutgoingMessageRecipientStateSkipped : OWSOutgoingMessageRecipientStateSending; recipientAddressStates[recipientAddress] = recipientState; } self.recipientAddressStates = [recipientAddressStates copy]; _outgoingMessageSchemaVersion = TSOutgoingMessageSchemaVersion; _changeActionsProtoData = outgoingMessageBuilder.changeActionsProtoData; return self; } #pragma mark - - (TSOutgoingMessageState)messageState { TSOutgoingMessageState newMessageState = [TSOutgoingMessage messageStateForRecipientStates:self.recipientAddressStates.allValues]; if (self.hasLegacyMessageState) { if (newMessageState == TSOutgoingMessageStateSent || self.legacyMessageState == TSOutgoingMessageStateSent) { return TSOutgoingMessageStateSent; } } return newMessageState; } - (BOOL)wasDeliveredToAnyRecipient { if (self.deliveredRecipientAddresses.count > 0) { return YES; } return (self.hasLegacyMessageState && self.legacyWasDelivered && self.messageState == TSOutgoingMessageStateSent); } - (BOOL)wasSentToAnyRecipient { if (self.sentRecipientAddresses.count > 0) { return YES; } return (self.hasLegacyMessageState && self.messageState == TSOutgoingMessageStateSent); } + (TSOutgoingMessageState)messageStateForRecipientStates:(NSArray *)recipientStates { OWSAssertDebug(recipientStates); // If there are any "sending" recipients, consider this message "sending". // If there are any "pending" recipients, consider this message "pending". BOOL hasFailed = NO; for (TSOutgoingMessageRecipientState *recipientState in recipientStates) { if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { return TSOutgoingMessageStateSending; } else if (recipientState.state == OWSOutgoingMessageRecipientStatePending) { return TSOutgoingMessageStatePending; } else if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) { hasFailed = YES; } } // If there are any "failed" recipients, consider this message "failed". if (hasFailed) { return TSOutgoingMessageStateFailed; } // Otherwise, consider the message "sent". // // NOTE: This includes messages with no recipients. return TSOutgoingMessageStateSent; } - (BOOL)shouldBeSaved { if (!super.shouldBeSaved) { return NO; } if (self.groupMetaMessage == TSGroupMetaMessageDeliver || self.groupMetaMessage == TSGroupMetaMessageUnspecified) { return YES; } // There's no need to save this message, since it's not displayed to the user. // // Should we find a need to save this in the future, we need to exclude any non-serializable properties. return NO; } - (void)anyWillInsertWithTransaction:(SDSAnyWriteTransaction *)transaction { [super anyWillInsertWithTransaction:transaction]; _storedMessageState = self.messageState; } - (void)anyDidInsertWithTransaction:(SDSAnyWriteTransaction *)transaction { [super anyDidInsertWithTransaction:transaction]; [self markMessageSendLogEntryCompleteIfNeededWithTx:transaction]; } - (void)anyWillUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction { [super anyWillUpdateWithTransaction:transaction]; _storedMessageState = self.messageState; } - (void)anyDidUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction { [super anyDidUpdateWithTransaction:transaction]; [self markMessageSendLogEntryCompleteIfNeededWithTx:transaction]; } // This method will be called after every insert and update, so it needs // to be cheap. - (BOOL)shouldStartExpireTimer { if (self.hasPerConversationExpirationStarted) { // Expiration already started. return YES; } else if (!self.hasPerConversationExpiration) { return NO; } else if (!super.shouldStartExpireTimer) { return NO; } // It's not clear if we should wait until _all_ recipients have reached "sent or later" // (which could never occur if one group member is unregistered) or only wait until // the first recipient has reached "sent or later" (which could cause partially delivered // messages to expire). For now, we'll do the latter. // // TODO: Revisit this decision. return self.messageState == TSOutgoingMessageStateSent; } - (BOOL)isOnline { return NO; } - (BOOL)isUrgent { return YES; } - (OWSInteractionType)interactionType { return OWSInteractionType_OutgoingMessage; } // MCR: Check what calls this method and if it needs to be changed. - (NSArray *)recipientAddresses { return self.recipientAddressStates.allKeys; } - (NSArray *)sendingRecipientAddresses { NSMutableArray *result = [NSMutableArray new]; for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) { TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress]; if (recipientState.state == OWSOutgoingMessageRecipientStateSending || recipientState.state == OWSOutgoingMessageRecipientStatePending) { [result addObject:recipientAddress]; } } return result; } - (NSArray *)sentRecipientAddresses { NSMutableArray *result = [NSMutableArray new]; for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) { TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress]; if (recipientState.state == OWSOutgoingMessageRecipientStateSent) { [result addObject:recipientAddress]; } } return result; } - (NSArray *)deliveredRecipientAddresses { NSMutableArray *result = [NSMutableArray new]; for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) { TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress]; if (recipientState.deliveryTimestamp != nil) { [result addObject:recipientAddress]; } } return result; } - (NSArray *)readRecipientAddresses { NSMutableArray *result = [NSMutableArray new]; for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) { TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress]; if (recipientState.readTimestamp != nil) { [result addObject:recipientAddress]; } } return result; } - (NSArray *)viewedRecipientAddresses { NSMutableArray *result = [NSMutableArray new]; for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) { TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress]; if (recipientState.viewedTimestamp != nil) { [result addObject:recipientAddress]; } } return result; } - (NSUInteger)sentRecipientsCount { return [self.recipientAddressStates .allValues filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( TSOutgoingMessageRecipientState *recipientState, NSDictionary *_Nullable bindings) { return recipientState.state == OWSOutgoingMessageRecipientStateSent; }]].count; } // MCR: Check what calls this and if it needs to be changed. - (nullable TSOutgoingMessageRecipientState *)recipientStateForAddress:(SignalServiceAddress *)address { OWSAssertDebug(address.isValid); TSOutgoingMessageRecipientState *_Nullable result = self.recipientAddressStates[address]; OWSAssertDebug(result); return [result copy]; } #pragma mark - Update With... Methods - (void)updateWithSendingError:(NSError *)error transaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(error); [self anyUpdateOutgoingMessageWithTransaction:transaction block:^(TSOutgoingMessage *message) { // Mark any "sending" recipients as "failed." for (TSOutgoingMessageRecipientState *recipientState in message .recipientAddressStates.allValues) { if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { recipientState.state = OWSOutgoingMessageRecipientStateFailed; } } [message setMostRecentFailureText:error.userErrorDescription]; }]; } - (void)updateWithAllSendingRecipientsMarkedAsFailedWithTransaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(transaction); [self anyUpdateOutgoingMessageWithTransaction:transaction block:^(TSOutgoingMessage *message) { // Mark any "sending" recipients as "failed." for (TSOutgoingMessageRecipientState *recipientState in message .recipientAddressStates.allValues) { if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { recipientState.state = OWSOutgoingMessageRecipientStateFailed; } } }]; } - (BOOL)hasFailedRecipients { for (TSOutgoingMessageRecipientState *recipientState in self.recipientAddressStates.allValues) { if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) { return YES; } } return NO; } - (void)updateAllUnsentRecipientsAsSendingWithTransaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(transaction); [self anyUpdateOutgoingMessageWithTransaction:transaction block:^(TSOutgoingMessage *message) { // Mark any "failed" recipients as "sending." for (TSOutgoingMessageRecipientState *recipientState in message .recipientAddressStates.allValues) { if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) { recipientState.state = OWSOutgoingMessageRecipientStateSending; } } }]; } - (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript transaction:(SDSAnyWriteTransaction *)transaction { [self anyUpdateOutgoingMessageWithTransaction:transaction block:^(TSOutgoingMessage *message) { [message setHasSyncedTranscript:hasSyncedTranscript]; }]; } - (void)updateWithSentRecipient:(ServiceIdObjC *)serviceId wasSentByUD:(BOOL)wasSentByUD transaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(serviceId); OWSAssertDebug(transaction); SignalServiceAddress *recipientAddress = [[SignalServiceAddress alloc] initWithServiceIdObjC:serviceId]; [self updateWithSentRecipientAddress:recipientAddress wasSentByUD:wasSentByUD transaction:transaction]; } - (void)updateWithSentRecipientAddress:(SignalServiceAddress *)recipientAddress wasSentByUD:(BOOL)wasSentByUD transaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(recipientAddress); OWSAssertDebug(transaction); [self anyUpdateOutgoingMessageWithTransaction:transaction block:^(TSOutgoingMessage *message) { TSOutgoingMessageRecipientState *_Nullable recipientState = message.recipientAddressStates[recipientAddress]; if (!recipientState) { OWSFailDebug( @"Missing recipient state for recipient: %@", recipientAddress); return; } recipientState.state = OWSOutgoingMessageRecipientStateSent; recipientState.wasSentByUD = wasSentByUD; recipientState.errorCode = nil; }]; } - (void)updateWithSkippedRecipient:(SignalServiceAddress *)recipientAddress transaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(recipientAddress.isValid); OWSAssertDebug(transaction); [self anyUpdateOutgoingMessageWithTransaction:transaction block:^(TSOutgoingMessage *message) { TSOutgoingMessageRecipientState *_Nullable recipientState = message.recipientAddressStates[recipientAddress]; if (!recipientState) { OWSFailDebug( @"Missing recipient state for recipient: %@", recipientAddress); return; } recipientState.state = OWSOutgoingMessageRecipientStateSkipped; }]; } - (void)updateWithFailedRecipient:(SignalServiceAddress *)recipientAddress error:(NSError *)error transaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(recipientAddress.isValid); OWSAssertDebug(transaction); OWSLogWarn(@"Send to recipient failed, address: %@, timestamp: %llu, error: %@ (isRetryable: %d)", recipientAddress, self.timestamp, error, error.isRetryable); [self anyUpdateOutgoingMessageWithTransaction:transaction block:^(TSOutgoingMessage *message) { TSOutgoingMessageRecipientState *_Nullable recipientState = message.recipientAddressStates[recipientAddress]; if (!recipientState) { OWSFailDebug( @"Missing recipient state for recipient: %@", recipientAddress); return; } if (error.isRetryable && recipientState.state == OWSOutgoingMessageRecipientStateSending) { // For retryable errors, we can just set the error code and leave the // state set as Sending } else if ([SpamChallengeRequiredError isSpamChallengeRequiredError:error] || [SpamChallengeResolvedError isSpamChallengeResolvedError:error]) { recipientState.state = OWSOutgoingMessageRecipientStatePending; } else { recipientState.state = OWSOutgoingMessageRecipientStateFailed; } recipientState.errorCode = @(error.code); }]; } - (void)updateRecipientsFromNonLocalDevice: (NSDictionary *)recipientStates isSentUpdate:(BOOL)isSentUpdate transaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(transaction); [self anyUpdateOutgoingMessageWithTransaction:transaction block:^(TSOutgoingMessage *message) { if (recipientStates.count > 0) { // If we have specific recipient info from the transcript, // build a new recipient state map. if (isSentUpdate) { // If this is a "sent update", make sure that: // // a) "Sent updates" should never remove any recipients. We end // up with the // union of the existing and new recipients. // b) "Sent updates" should never downgrade the "recipient // state" for any // recipients. Prefer existing "recipient state"; "sent // updates" only add new recipients at the "sent" state. // // Therefore we retain all existing entries in the recipient // state map. NSMutableDictionary *recipientAddressStates = recipientStates.mutableCopy; [recipientAddressStates addEntriesFromDictionary:self.recipientAddressStates]; [message setRecipientAddressStates:recipientAddressStates]; } else { [message setRecipientAddressStates:recipientStates]; } } else { // Otherwise assume this is a legacy message before UD was // introduced, and mark any "sending" recipient as "sent". Note // that this will apply to non-legacy messages with no recipients. for (TSOutgoingMessageRecipientState *recipientState in message .recipientAddressStates.allValues) { if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { recipientState.state = OWSOutgoingMessageRecipientStateSent; } } } if (!isSentUpdate) { [message setWasNotCreatedLocally:YES]; } }]; } - (void)updateWithRecipientAddressStates: (nullable NSDictionary *)recipientAddressStates transaction:(SDSAnyWriteTransaction *)transaction { [self anyUpdateOutgoingMessageWithTransaction:transaction block:^(TSOutgoingMessage *message) { message.recipientAddressStates = [recipientAddressStates copy]; }]; } #ifdef TESTABLE_BUILD - (void)updateWithFakeMessageState:(TSOutgoingMessageState)messageState transaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(transaction); [self anyUpdateOutgoingMessageWithTransaction:transaction block:^(TSOutgoingMessage *message) { for (TSOutgoingMessageRecipientState *recipientState in message .recipientAddressStates.allValues) { switch (messageState) { case TSOutgoingMessageStateSending: recipientState.state = OWSOutgoingMessageRecipientStateSending; break; case TSOutgoingMessageStateFailed: recipientState.state = OWSOutgoingMessageRecipientStateFailed; break; case TSOutgoingMessageStateSent: recipientState.state = OWSOutgoingMessageRecipientStateSent; break; case TSOutgoingMessageStatePending: recipientState.state = OWSOutgoingMessageRecipientStatePending; break; default: OWSFailDebug(@"unexpected message state."); break; } } }]; } #endif #pragma mark - - (nullable SSKProtoDataMessageBuilder *)dataMessageBuilderWithThread:(TSThread *)thread transaction:(SDSAnyReadTransaction *)transaction { OWSAssertDebug(thread); SSKProtoDataMessageBuilder *builder = [SSKProtoDataMessage builder]; [builder setTimestamp:self.timestamp]; NSUInteger requiredProtocolVersion = SSKProtoDataMessageProtocolVersionInitial; if (self.isViewOnceMessage) { [builder setIsViewOnce:YES]; requiredProtocolVersion = SSKProtoDataMessageProtocolVersionViewOnceVideo; } NSString *body = self.body; NSString *trimmedBody = [body trimToUtf8ByteCount:(NSInteger)kOversizeTextMessageSizeThreshold]; OWSAssertDebug(body.length == trimmedBody.length); [builder setBody:trimmedBody]; NSArray *bodyRanges = [self.bodyRanges toProtoBodyRangesWithBodyLength:(NSInteger)self.body.length]; if (bodyRanges.count > 0) { [builder setBodyRanges:bodyRanges]; if (requiredProtocolVersion < SSKProtoDataMessageProtocolVersionMentions) { requiredProtocolVersion = SSKProtoDataMessageProtocolVersionMentions; } } // Story Context if (self.storyTimestamp && self.storyAuthorUuidString) { if (self.storyReactionEmoji) { SSKProtoDataMessageReactionBuilder *reactionBuilder = [SSKProtoDataMessageReaction builderWithEmoji:self.storyReactionEmoji timestamp:self.storyTimestamp.unsignedLongLongValue]; // ACI TODO: Use `serviceIdString` to populate this value. [reactionBuilder setTargetAuthorAci:self.storyAuthorUuidString]; NSError *error; SSKProtoDataMessageReaction *_Nullable reaction = [reactionBuilder buildAndReturnError:&error]; if (error || !reaction) { OWSFailDebug(@"Could not build story reaction protobuf: %@.", error); } else { [builder setReaction:reaction]; if (requiredProtocolVersion < SSKProtoDataMessageProtocolVersionReactions) { requiredProtocolVersion = SSKProtoDataMessageProtocolVersionReactions; } } } SSKProtoDataMessageStoryContextBuilder *storyContextBuilder = [SSKProtoDataMessageStoryContext builder]; // ACI TODO: Use `serviceIdString` to populate this value. [storyContextBuilder setAuthorAci:self.storyAuthorUuidString]; [storyContextBuilder setSentTimestamp:self.storyTimestamp.unsignedLongLongValue]; [builder setStoryContext:[storyContextBuilder buildInfallibly]]; } [builder setExpireTimer:self.expiresInSeconds]; // Group Messages if ([thread isKindOfClass:[TSGroupThread class]]) { TSGroupThread *groupThread = (TSGroupThread *)thread; OutgoingGroupProtoResult result; switch (groupThread.groupModel.groupsVersion) { case GroupsVersionV1: OWSLogError(@"[GV1] Cannot build data message for V1 group!"); result = OutgoingGroupProtoResult_Error; break; case GroupsVersionV2: result = [self addGroupsV2ToDataMessageBuilder:builder groupThread:groupThread tx:transaction]; break; } switch (result) { case OutgoingGroupProtoResult_Error: return nil; case OutgoingGroupProtoResult_AddedWithoutGroupAvatar: break; } } // Message Attachments NSError *bodyError; NSArray *attachments = [self buildProtosForBodyAttachmentsWithTx:transaction error:&bodyError]; if (bodyError) { OWSFailDebug(@"Could not build body attachments"); } else { [builder setAttachments:attachments]; } // Quoted Reply if (self.quotedMessage) { NSError *error; SSKProtoDataMessageQuote *_Nullable quoteProto = [self buildQuoteProtoWithQuote:self.quotedMessage tx:transaction error:&error]; if (error || !quoteProto) { OWSFailDebug(@"Could not build quote protobuf: %@.", error); } else { [builder setQuote:quoteProto]; if (quoteProto.bodyRanges.count > 0) { if (requiredProtocolVersion < SSKProtoDataMessageProtocolVersionMentions) { requiredProtocolVersion = SSKProtoDataMessageProtocolVersionMentions; } } } } // Contact Share if (self.contactShare) { NSError *error; SSKProtoDataMessageContact *_Nullable contactProto = [self buildContactShareProto:self.contactShare tx:transaction error:&error]; if (error || !contactProto) { OWSFailDebug(@"Could not build contact share protobuf: %@.", error); } else { [builder addContact:contactProto]; } } // Link Preview if (self.linkPreview) { NSError *error; SSKProtoPreview *_Nullable previewProto = [self buildLinkPreviewProtoWithLinkPreview:self.linkPreview tx:transaction error:&error]; if (error || !previewProto) { OWSFailDebug(@"Could not build link preview protobuf: %@.", error); } else { [builder addPreview:previewProto]; } } // Sticker if (self.messageSticker) { NSError *error; SSKProtoDataMessageSticker *_Nullable stickerProto = [self buildStickerProtoWithSticker:self.messageSticker tx:transaction error:&error]; if (error || !stickerProto) { OWSFailDebug(@"Could not build sticker protobuf: %@.", error); } else { [builder setSticker:stickerProto]; } } // Gift badge if (self.giftBadge) { SSKProtoDataMessageGiftBadgeBuilder *giftBadgeBuilder = [SSKProtoDataMessageGiftBadge builder]; [giftBadgeBuilder setReceiptCredentialPresentation:self.giftBadge.redemptionCredential]; [builder setGiftBadge:[giftBadgeBuilder buildInfallibly]]; } [builder setRequiredProtocolVersion:(uint32_t)requiredProtocolVersion]; return builder; } // recipientId is nil when building "sent" sync messages for messages sent to groups. - (nullable SSKProtoDataMessage *)buildDataMessage:(TSThread *)thread transaction:(SDSAnyReadTransaction *)transaction { OWSAssertDebug(thread); OWSAssertDebug([thread.uniqueId isEqualToString:self.uniqueThreadId]); SSKProtoDataMessageBuilder *_Nullable builder = [self dataMessageBuilderWithThread:thread transaction:transaction]; if (!builder) { OWSFailDebug(@"could not build protobuf."); return nil; } [ProtoUtils addLocalProfileKeyIfNecessary:thread dataMessageBuilder:builder transaction:transaction]; NSError *error; SSKProtoDataMessage *_Nullable dataProto = [builder buildAndReturnError:&error]; if (error || !dataProto) { OWSFailDebug(@"could not build protobuf: %@", error); return nil; } return dataProto; } - (nullable SSKProtoContentBuilder *)contentBuilderWithThread:(TSThread *)thread transaction:(SDSAnyReadTransaction *)transaction { SSKProtoDataMessage *_Nullable dataMessage = [self buildDataMessage:thread transaction:transaction]; if (!dataMessage) { return nil; } SSKProtoContentBuilder *contentBuilder = [SSKProtoContent builder]; [contentBuilder setDataMessage:dataMessage]; return contentBuilder; } - (nullable NSData *)buildPlainTextData:(TSThread *)thread transaction:(SDSAnyWriteTransaction *)transaction { SSKProtoContentBuilder *_Nullable contentBuilder = [self contentBuilderWithThread:thread transaction:transaction]; if (!contentBuilder) { OWSFailDebug(@"could not build protobuf."); return nil; } [contentBuilder setPniSignatureMessage:[self buildPniSignatureMessageIfNeededWithTransaction:transaction]]; NSError *error; NSData *_Nullable contentData = [contentBuilder buildSerializedDataAndReturnError:&error]; if (error || !contentData) { OWSFailDebug(@"could not serialize protobuf: %@", error); return nil; } return contentData; } - (BOOL)shouldSyncTranscript { return YES; } - (nullable OWSOutgoingSyncMessage *)buildTranscriptSyncMessageWithLocalThread:(TSThread *)localThread transaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(self.shouldSyncTranscript); TSThread *messageThread = [self threadWithTx:transaction]; if (messageThread == nil) { return nil; } return [[OWSOutgoingSentMessageTranscript alloc] initWithLocalThread:localThread messageThread:messageThread outgoingMessage:self isRecipientUpdate:self.hasSyncedTranscript transaction:transaction]; } - (NSString *)statusDescription { NSMutableString *result = [NSMutableString new]; [result appendFormat:@"[status: %@\n", NSStringForOutgoingMessageState(self.messageState)]; for (SignalServiceAddress *address in self.recipientAddressStates) { TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[address]; [result appendFormat:@", %@: %@\n", address, NSStringForOutgoingMessageRecipientState(recipientState.state)]; } [result appendString:@"]"]; return [result copy]; } @end NS_ASSUME_NONNULL_END