Signal-iOS/SignalServiceKit/Messages/Interactions/TSOutgoingMessage.m

1137 lines
49 KiB
Objective-C

//
// 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 <SignalCoreKit/NSDate+OWS.h>
#import <SignalCoreKit/NSString+OWS.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
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<NSString *> *)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<SignalServiceAddress *,TSOutgoingMessageRecipientState *> *)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
attachmentId:(nullable NSString *)attachmentId
{
return [self outgoingMessageInThread:thread
messageBody:body
attachmentId:attachmentId
expiresInSeconds:0
quotedMessage:nil
linkPreview:nil
messageSticker:nil];
}
+ (instancetype)outgoingMessageInThread:(TSThread *)thread
messageBody:(nullable NSString *)body
attachmentId:(nullable NSString *)attachmentId
expiresInSeconds:(uint32_t)expiresInSeconds
{
return [self outgoingMessageInThread:thread
messageBody:body
attachmentId:attachmentId
expiresInSeconds:expiresInSeconds
quotedMessage:nil
linkPreview:nil
messageSticker:nil];
}
+ (instancetype)outgoingMessageInThread:(TSThread *)thread
messageBody:(nullable NSString *)body
attachmentId:(nullable NSString *)attachmentId
expiresInSeconds:(uint32_t)expiresInSeconds
quotedMessage:(nullable TSQuotedMessage *)quotedMessage
linkPreview:(nullable OWSLinkPreview *)linkPreview
messageSticker:(nullable MessageSticker *)messageSticker
{
NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
if (attachmentId) {
[attachmentIds addObject:attachmentId];
}
TSOutgoingMessageBuilder *builder = [TSOutgoingMessageBuilder outgoingMessageBuilderWithThread:thread];
builder.messageBody = body;
builder.attachmentIds = attachmentIds;
builder.expiresInSeconds = expiresInSeconds;
builder.quotedMessage = quotedMessage;
builder.linkPreview = linkPreview;
builder.messageSticker = messageSticker;
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<SignalServiceAddress *> *recipientAddresses = [NSMutableSet new];
if ([self isKindOfClass:[OWSOutgoingSyncMessage class]]) {
// 1. Sync messages should only be sent to linked devices.
SignalServiceAddress *localAddress = [TSAccountManagerObjcBridge localAciAddressWith:transaction];
OWSAssertDebug(localAddress);
[recipientAddresses addObject:localAddress];
} else {
// 2. Most messages should only be sent to the current members of the group.
[recipientAddresses addObjectsFromArray:[thread recipientAddressesWithTransaction:transaction]];
// 3. V2 group updates should also be sent to pending members of the group
// whose clients support Groups v2.
if (outgoingMessageBuilder.additionalRecipients) {
[recipientAddresses addObjectsFromArray:outgoingMessageBuilder.additionalRecipients];
}
}
NSMutableDictionary<SignalServiceAddress *, TSOutgoingMessageRecipientState *> *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<TSOutgoingMessageRecipientState *> *)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<SignalServiceAddress *> *)recipientAddresses
{
return self.recipientAddressStates.allKeys;
}
- (NSArray<SignalServiceAddress *> *)sendingRecipientAddresses
{
NSMutableArray<SignalServiceAddress *> *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<SignalServiceAddress *> *)sentRecipientAddresses
{
NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
if (recipientState.state == OWSOutgoingMessageRecipientStateSent) {
[result addObject:recipientAddress];
}
}
return result;
}
- (NSArray<SignalServiceAddress *> *)deliveredRecipientAddresses
{
NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
if (recipientState.deliveryTimestamp != nil) {
[result addObject:recipientAddress];
}
}
return result;
}
- (NSArray<SignalServiceAddress *> *)readRecipientAddresses
{
NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
if (recipientState.readTimestamp != nil) {
[result addObject:recipientAddress];
}
}
return result;
}
- (NSArray<SignalServiceAddress *> *)viewedRecipientAddresses
{
NSMutableArray<SignalServiceAddress *> *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<NSString *, id> *_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<SignalServiceAddress *, TSOutgoingMessageRecipientState *> *)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<SignalServiceAddress *,
TSOutgoingMessageRecipientState *> *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<SignalServiceAddress *, TSOutgoingMessageRecipientState *> *)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;
}
if ([self.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold) {
[builder setBody:self.body];
} else {
OWSFailDebug(@"message body length too long.");
NSString *truncatedBody = [self.body copy];
while ([truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kOversizeTextMessageSizeThreshold) {
OWSLogError(@"truncating body which is too long: %lu",
(unsigned long)[truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
truncatedBody = [truncatedBody substringToIndex:truncatedBody.length / 2];
}
[builder setBody:truncatedBody];
}
NSArray<SSKProtoBodyRange *> *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
NSArray<SSKProtoAttachmentPointer *> *attachments = [self buildProtosForBodyAttachmentsWithTx:transaction];
[builder setAttachments:attachments];
// Quoted Reply
SSKProtoDataMessageQuoteBuilder *_Nullable quotedMessageBuilder =
[self quotedMessageBuilderWithTransaction:transaction];
if (quotedMessageBuilder) {
NSError *error;
SSKProtoDataMessageQuote *_Nullable quoteProto = [quotedMessageBuilder buildAndReturnError:&error];
if (error || !quoteProto) {
OWSFailDebug(@"could not build protobuf: %@.", error);
return nil;
}
[builder setQuote:quoteProto];
if (quoteProto.bodyRanges.count > 0) {
if (requiredProtocolVersion < SSKProtoDataMessageProtocolVersionMentions) {
requiredProtocolVersion = SSKProtoDataMessageProtocolVersionMentions;
}
}
}
// Contact Share
if (self.contactShare) {
SSKProtoDataMessageContact *_Nullable contactProto =
[self.contactShare buildProtoWithParentMessage:self tx:transaction];
if (contactProto) {
[builder addContact:contactProto];
} else {
OWSFailDebug(@"contactProto was unexpectedly nil");
}
}
// 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) {
SSKProtoAttachmentPointer *_Nullable attachmentProto = [self buildProtoForStickerAttachmentWithTx:transaction];
if (!attachmentProto) {
OWSFailDebug(@"Could not build sticker attachment protobuf.");
} else {
SSKProtoDataMessageStickerBuilder *stickerBuilder =
[SSKProtoDataMessageSticker builderWithPackID:self.messageSticker.packId
packKey:self.messageSticker.packKey
stickerID:self.messageSticker.stickerId
data:attachmentProto];
if (self.messageSticker.emoji.length > 0) {
[stickerBuilder setEmoji:self.messageSticker.emoji];
}
NSError *error;
SSKProtoDataMessageSticker *_Nullable stickerProto = [stickerBuilder buildAndReturnError:&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;
}
- (nullable SSKProtoDataMessageQuoteBuilder *)quotedMessageBuilderWithTransaction:(SDSAnyReadTransaction *)transaction
{
if (!self.quotedMessage) {
return nil;
}
TSQuotedMessage *quotedMessage = self.quotedMessage;
if (!quotedMessage.timestampValue) {
return nil;
}
uint64_t timestamp = quotedMessage.timestampValue.unsignedLongLongValue;
SSKProtoDataMessageQuoteBuilder *quoteBuilder = [SSKProtoDataMessageQuote builderWithId:timestamp];
if (quotedMessage.authorAddress.aciString) {
[quoteBuilder setAuthorAci:quotedMessage.authorAddress.aciString];
} else {
OWSFailDebug(@"It should be impossible to quote a message without a UUID");
return nil;
}
BOOL hasQuotedText = NO;
BOOL hasQuotedAttachment = NO;
BOOL hasQuotedGiftBadge = NO;
if (quotedMessage.body.length > 0) {
hasQuotedText = YES;
[quoteBuilder setText:quotedMessage.body];
NSArray<SSKProtoBodyRange *> *bodyRanges =
[self.quotedMessage.bodyRanges toProtoBodyRangesWithBodyLength:(NSInteger)self.quotedMessage.body.length];
if (bodyRanges.count > 0) {
[quoteBuilder setBodyRanges:bodyRanges];
}
}
SSKProtoDataMessageQuoteQuotedAttachment *attachmentProto =
[self buildQuotedMessageAttachmentProtoWithTx:transaction];
if (attachmentProto) {
[quoteBuilder addAttachments:attachmentProto];
hasQuotedAttachment = YES;
}
if (quotedMessage.isGiftBadge) {
[quoteBuilder setType:SSKProtoDataMessageQuoteTypeGiftBadge];
hasQuotedGiftBadge = YES;
}
if (hasQuotedText || hasQuotedAttachment || hasQuotedGiftBadge) {
return quoteBuilder;
} else {
OWSFailDebug(@"Invalid quoted message data.");
return nil;
}
}
// 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