session-ios/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m

1019 lines
41 KiB
Mathematica
Raw Normal View History

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
2015-12-07 03:31:43 +01:00
#import "TSOutgoingMessage.h"
#import "NSDate+OWS.h"
2018-04-30 22:42:14 +02:00
#import "NSString+SSK.h"
2018-05-01 14:52:59 +02:00
#import "OWSContact.h"
2018-04-10 18:59:26 +02:00
#import "OWSMessageSender.h"
#import "OWSOutgoingSyncMessage.h"
2018-04-23 16:53:43 +02:00
#import "OWSPrimaryStorage.h"
#import "ProtoUtils.h"
#import "SignalRecipient.h"
#import "TSAccountManager.h"
#import "TSAttachmentStream.h"
#import "TSContactThread.h"
#import "TSGroupThread.h"
#import "TSQuotedMessage.h"
#import "TextSecureKitEnv.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
#import <YapDatabase/YapDatabase.h>
#import <YapDatabase/YapDatabaseTransaction.h>
2015-12-07 03:31:43 +01:00
NS_ASSUME_NONNULL_BEGIN
NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRecipientAll";
2018-04-30 16:21:58 +02:00
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";
}
}
NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientState value)
{
switch (value) {
case OWSOutgoingMessageRecipientStateFailed:
return @"OWSOutgoingMessageRecipientStateFailed";
case OWSOutgoingMessageRecipientStateSending:
return @"OWSOutgoingMessageRecipientStateSending";
case OWSOutgoingMessageRecipientStateSkipped:
return @"OWSOutgoingMessageRecipientStateSkipped";
case OWSOutgoingMessageRecipientStateSent:
return @"OWSOutgoingMessageRecipientStateSent";
}
}
2018-04-23 16:30:51 +02:00
@interface TSOutgoingMessageRecipientState ()
@property (atomic) OWSOutgoingMessageRecipientState state;
@property (atomic, nullable) NSNumber *deliveryTimestamp;
@property (atomic, nullable) NSNumber *readTimestamp;
@end
#pragma mark -
@implementation TSOutgoingMessageRecipientState
@end
#pragma mark -
@interface TSOutgoingMessage ()
@property (atomic) BOOL hasSyncedTranscript;
@property (atomic) NSString *customMessage;
@property (atomic) NSString *mostRecentFailureText;
@property (atomic) BOOL isFromLinkedDevice;
@property (atomic) TSGroupMetaMessage groupMetaMessage;
@property (nonatomic, readonly) TSOutgoingMessageState legacyMessageState;
@property (nonatomic, readonly) BOOL legacyWasDelivered;
@property (nonatomic, readonly) BOOL hasLegacyMessageState;
2018-04-23 16:30:51 +02:00
@property (atomic, nullable) NSDictionary<NSString *, TSOutgoingMessageRecipientState *> *recipientStateMap;
@end
#pragma mark -
2015-12-07 03:31:43 +01:00
@implementation TSOutgoingMessage
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
2017-04-13 18:54:03 +02:00
self = [super initWithCoder:coder];
2017-04-13 18:54:03 +02:00
if (self) {
if (!_attachmentFilenameMap) {
_attachmentFilenameMap = [NSMutableDictionary new];
}
2018-04-23 16:30:51 +02:00
if (!self.recipientStateMap) {
[self migrateRecipientStateMapWithCoder:coder];
OWSAssert(self.recipientStateMap);
}
2017-04-13 18:54:03 +02:00
}
2017-04-13 18:54:03 +02:00
return self;
}
2018-04-23 16:30:51 +02:00
- (void)migrateRecipientStateMapWithCoder:(NSCoder *)coder
{
OWSAssert(!self.recipientStateMap);
OWSAssert(coder);
// Determine the "overall message state."
TSOutgoingMessageState oldMessageState = TSOutgoingMessageStateFailed;
NSNumber *_Nullable messageStateValue = [coder decodeObjectForKey:@"messageState"];
if (messageStateValue) {
oldMessageState = (TSOutgoingMessageState)messageStateValue.intValue;
}
_hasLegacyMessageState = YES;
_legacyMessageState = oldMessageState;
2018-04-23 16:30:51 +02:00
OWSOutgoingMessageRecipientState defaultState;
switch (oldMessageState) {
case TSOutgoingMessageStateFailed:
defaultState = OWSOutgoingMessageRecipientStateFailed;
break;
case TSOutgoingMessageStateSending:
defaultState = OWSOutgoingMessageRecipientStateSending;
break;
case TSOutgoingMessageStateSent:
case TSOutgoingMessageStateSent_OBSOLETE:
case TSOutgoingMessageStateDelivered_OBSOLETE:
// Convert legacy values.
defaultState = OWSOutgoingMessageRecipientStateSent;
break;
}
// Try to leverage the "per-recipient state."
NSDictionary<NSString *, NSNumber *> *_Nullable recipientDeliveryMap =
[coder decodeObjectForKey:@"recipientDeliveryMap"];
NSDictionary<NSString *, NSNumber *> *_Nullable recipientReadMap = [coder decodeObjectForKey:@"recipientReadMap"];
NSArray<NSString *> *_Nullable sentRecipients = [coder decodeObjectForKey:@"sentRecipients"];
NSMutableDictionary<NSString *, TSOutgoingMessageRecipientState *> *recipientStateMap = [NSMutableDictionary new];
__block BOOL isGroupThread = NO;
2018-04-23 16:30:51 +02:00
// Our default recipient list is the current thread members.
2018-04-23 16:53:43 +02:00
__block NSArray<NSString *> *recipientIds = @[];
// To avoid deadlock while migrating these records, we use a dedicated
// migration connection. For legacy records (created more than ~9 months
// before the migration), we need to infer the recipient list for this
// message from the current thread membership. This inference isn't
// always accurate, so not using the same connection for both reads is
// acceptable.
[TSOutgoingMessage.dbMigrationConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
TSThread *thread = [self threadWithTransaction:transaction];
recipientIds = [thread recipientIdentifiers];
isGroupThread = [thread isGroupThread];
2018-04-23 16:53:43 +02:00
}];
NSNumber *_Nullable wasDelivered = [coder decodeObjectForKey:@"wasDelivered"];
_legacyWasDelivered = wasDelivered && wasDelivered.boolValue;
BOOL wasDeliveredToContact = NO;
if (isGroupThread) {
2018-04-23 16:30:51 +02:00
// If we have a `sentRecipients` list, prefer that as it is more accurate.
if (sentRecipients) {
recipientIds = sentRecipients;
}
} else {
// Special-case messages in contact threads; if "was delivered", we know
// it was delivered to the contact.
wasDeliveredToContact = _legacyWasDelivered;
2018-04-23 16:30:51 +02:00
}
2018-04-23 16:30:51 +02:00
NSString *_Nullable singleGroupRecipient = [coder decodeObjectForKey:@"singleGroupRecipient"];
if (singleGroupRecipient) {
OWSFailDebug(@"unexpected single group recipient message.");
2018-04-23 16:30:51 +02:00
// If this is a "single group recipient message", treat it as such.
recipientIds = @[
singleGroupRecipient,
];
}
for (NSString *recipientId in recipientIds) {
TSOutgoingMessageRecipientState *recipientState = [TSOutgoingMessageRecipientState new];
NSNumber *_Nullable readTimestamp = recipientReadMap[recipientId];
NSNumber *_Nullable deliveryTimestamp = recipientDeliveryMap[recipientId];
if (readTimestamp) {
// If we have a read timestamp for this recipient, mark it as read.
recipientState.state = OWSOutgoingMessageRecipientStateSent;
recipientState.readTimestamp = readTimestamp;
// deliveryTimestamp might be nil here.
recipientState.deliveryTimestamp = deliveryTimestamp;
} else if (deliveryTimestamp) {
// If we have a delivery timestamp for this recipient, mark it as delivered.
recipientState.state = OWSOutgoingMessageRecipientStateSent;
recipientState.deliveryTimestamp = deliveryTimestamp;
} else if (wasDeliveredToContact) {
OWSAssert(!isGroupThread);
recipientState.state = OWSOutgoingMessageRecipientStateSent;
// Use message time as an estimate of delivery time.
recipientState.deliveryTimestamp = @(self.timestamp);
2018-04-23 16:30:51 +02:00
} else if ([sentRecipients containsObject:recipientId]) {
// If this recipient is in `sentRecipients`, mark it as sent.
recipientState.state = OWSOutgoingMessageRecipientStateSent;
} else {
// Use the default state for this message.
recipientState.state = defaultState;
}
recipientStateMap[recipientId] = recipientState;
}
self.recipientStateMap = [recipientStateMap copy];
2018-04-23 16:53:43 +02:00
}
2018-04-23 16:30:51 +02:00
2018-04-23 16:53:43 +02:00
+ (YapDatabaseConnection *)dbMigrationConnection
{
static YapDatabaseConnection *connection = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
connection = [[OWSPrimaryStorage sharedManager] newDatabaseConnection];
});
return connection;
2018-04-23 16:30:51 +02:00
}
+ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread
messageBody:(nullable NSString *)body
attachmentId:(nullable NSString *)attachmentId
{
2018-04-06 16:59:23 +02:00
return [self outgoingMessageInThread:thread
messageBody:body
attachmentId:attachmentId
expiresInSeconds:0
quotedMessage:nil];
}
+ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread
messageBody:(nullable NSString *)body
attachmentId:(nullable NSString *)attachmentId
expiresInSeconds:(uint32_t)expiresInSeconds
2018-04-06 16:59:23 +02:00
{
return [self outgoingMessageInThread:thread
messageBody:body
attachmentId:attachmentId
expiresInSeconds:expiresInSeconds
quotedMessage:nil];
}
+ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread
messageBody:(nullable NSString *)body
attachmentId:(nullable NSString *)attachmentId
expiresInSeconds:(uint32_t)expiresInSeconds
quotedMessage:(nullable TSQuotedMessage *)quotedMessage
{
NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
if (attachmentId) {
[attachmentIds addObject:attachmentId];
}
return [[TSOutgoingMessage alloc] initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp]
inThread:thread
messageBody:body
attachmentIds:attachmentIds
expiresInSeconds:expiresInSeconds
expireStartedAt:0
isVoiceMessage:NO
groupMetaMessage:TSGroupMessageUnspecified
2018-04-27 21:29:19 +02:00
quotedMessage:quotedMessage
contactShare:nil];
}
+ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread
groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage
expiresInSeconds:(uint32_t)expiresInSeconds;
{
return [[TSOutgoingMessage alloc] initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp]
inThread:thread
messageBody:nil
attachmentIds:[NSMutableArray new]
expiresInSeconds:expiresInSeconds
expireStartedAt:0
isVoiceMessage:NO
groupMetaMessage:groupMetaMessage
2018-04-27 21:29:19 +02:00
quotedMessage:nil
contactShare:nil];
}
- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp
inThread:(nullable TSThread *)thread
messageBody:(nullable NSString *)body
attachmentIds:(NSMutableArray<NSString *> *)attachmentIds
expiresInSeconds:(uint32_t)expiresInSeconds
expireStartedAt:(uint64_t)expireStartedAt
isVoiceMessage:(BOOL)isVoiceMessage
groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage
quotedMessage:(nullable TSQuotedMessage *)quotedMessage
contactShare:(nullable OWSContact *)contactShare
{
self = [super initMessageWithTimestamp:timestamp
inThread:thread
messageBody:body
attachmentIds:attachmentIds
expiresInSeconds:expiresInSeconds
expireStartedAt:expireStartedAt
2018-04-27 21:29:19 +02:00
quotedMessage:quotedMessage
contactShare:contactShare];
if (!self) {
return self;
}
_hasSyncedTranscript = NO;
if ([thread isKindOfClass:TSGroupThread.class]) {
// Unless specified, we assume group messages are "Delivery" i.e. normal messages.
if (groupMetaMessage == TSGroupMessageUnspecified) {
_groupMetaMessage = TSGroupMessageDeliver;
} else {
_groupMetaMessage = groupMetaMessage;
}
} else {
OWSAssert(groupMetaMessage == TSGroupMessageUnspecified);
// Specifying a group meta message only makes sense for Group threads
_groupMetaMessage = TSGroupMessageUnspecified;
}
_isVoiceMessage = isVoiceMessage;
2017-04-13 18:54:03 +02:00
_attachmentFilenameMap = [NSMutableDictionary new];
2015-12-07 03:31:43 +01:00
2018-04-24 16:36:17 +02:00
// New outgoing messages should immediately determine their
// recipient list from current thread state.
2018-04-23 16:30:51 +02:00
NSMutableDictionary<NSString *, TSOutgoingMessageRecipientState *> *recipientStateMap = [NSMutableDictionary new];
NSArray<NSString *> *recipientIds;
if ([self isKindOfClass:[OWSOutgoingSyncMessage class]]) {
NSString *_Nullable localNumber = [TSAccountManager localNumber];
OWSAssert(localNumber);
recipientIds = @[
localNumber,
];
} else {
recipientIds = [thread recipientIdentifiers];
}
2018-04-23 16:30:51 +02:00
for (NSString *recipientId in recipientIds) {
TSOutgoingMessageRecipientState *recipientState = [TSOutgoingMessageRecipientState new];
recipientState.state = OWSOutgoingMessageRecipientStateSending;
recipientStateMap[recipientId] = recipientState;
}
self.recipientStateMap = [recipientStateMap copy];
2015-12-07 03:31:43 +01:00
return self;
}
2018-07-30 16:22:45 +02:00
- (void)dealloc
{
2018-08-01 15:47:54 +02:00
[self removeTemporaryAttachments];
2018-07-30 16:22:45 +02:00
}
// Each message has the responsibility for eagerly cleaning up its attachments.
// Normally this is done in [TSMessage removeWithTransaction], but that doesn't
// apply for "transient", unsaved messages (i.e. shouldBeSaved == NO). These
// messages should clean up their attachments upon deallocation.
2018-08-01 15:47:54 +02:00
- (void)removeTemporaryAttachments
2018-07-30 16:22:45 +02:00
{
if (self.shouldBeSaved) {
2018-08-01 15:47:54 +02:00
// Message is not transient; no need to clean up attachments.
2018-07-30 16:22:45 +02:00
return;
}
NSArray<NSString *> *_Nullable attachmentIds = self.attachmentIds;
if (attachmentIds.count < 1) {
return;
}
[self.dbReadWriteConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (NSString *attachmentId in attachmentIds) {
// We need to fetch each attachment, since [TSAttachment removeWithTransaction:] does important work.
TSAttachment *_Nullable attachment =
[TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
if (!attachment) {
OWSLogError(@"Couldn't load interaction's attachment for deletion.");
2018-07-30 16:22:45 +02:00
continue;
}
[attachment removeWithTransaction:transaction];
};
}];
}
#pragma mark -
2018-04-23 16:30:51 +02:00
- (TSOutgoingMessageState)messageState
{
TSOutgoingMessageState newMessageState =
[TSOutgoingMessage messageStateForRecipientStates:self.recipientStateMap.allValues];
if (self.hasLegacyMessageState) {
if (newMessageState == TSOutgoingMessageStateSent || self.legacyMessageState == TSOutgoingMessageStateSent) {
return TSOutgoingMessageStateSent;
}
}
return newMessageState;
}
- (BOOL)wasDeliveredToAnyRecipient
{
if ([self deliveredRecipientIds].count > 0) {
return YES;
}
return (self.hasLegacyMessageState && self.legacyWasDelivered && self.messageState == TSOutgoingMessageStateSent);
2018-04-23 16:30:51 +02:00
}
+ (TSOutgoingMessageState)messageStateForRecipientStates:(NSArray<TSOutgoingMessageRecipientState *> *)recipientStates
{
OWSAssert(recipientStates);
// If there are any "sending" recipients, consider this message "sending".
BOOL hasFailed = NO;
for (TSOutgoingMessageRecipientState *recipientState in recipientStates) {
if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
return TSOutgoingMessageStateSending;
} 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 (self.groupMetaMessage == TSGroupMessageDeliver || self.groupMetaMessage == TSGroupMessageUnspecified) {
return YES;
2017-11-17 16:19:52 +01:00
}
return NO;
}
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
if (!self.shouldBeSaved) {
// 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.
OWSLogDebug(@"Skipping save for group meta message.");
return;
}
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
[super saveWithTransaction:transaction];
}
2018-05-03 01:15:26 +02:00
- (BOOL)shouldStartExpireTimerWithTransaction:(YapDatabaseReadTransaction *)transaction
{
2018-04-23 16:30:51 +02:00
// 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.
if (!self.isExpiringMessage) {
return NO;
} else if (self.messageState == TSOutgoingMessageStateSent) {
return YES;
2018-04-23 16:30:51 +02:00
} else {
if (self.expireStartedAt > 0) {
// Our initial migration to populate the recipient state map was incomplete. It's since been
// addressed, but it's possible there are edge cases where a previously sent message would
// no longer be considered sent.
// So here we take extra care not to stop any expiration that had previously started.
// This can also happen under normal cirumstances with an outgoing group message.
OWSLogWarn(@"expiration previously started");
return YES;
}
return NO;
}
}
- (BOOL)isSilent
{
return NO;
}
2017-10-10 22:13:54 +02:00
- (OWSInteractionType)interactionType
{
return OWSInteractionType_OutgoingMessage;
}
2018-04-23 16:30:51 +02:00
- (NSArray<NSString *> *)recipientIds
{
2018-04-24 22:11:10 +02:00
return self.recipientStateMap.allKeys;
2018-04-23 16:30:51 +02:00
}
- (NSArray<NSString *> *)sendingRecipientIds
{
NSMutableArray<NSString *> *result = [NSMutableArray new];
for (NSString *recipientId in self.recipientStateMap) {
TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId];
if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
[result addObject:recipientId];
}
}
return result;
}
- (NSArray<NSString *> *)deliveredRecipientIds
{
NSMutableArray<NSString *> *result = [NSMutableArray new];
for (NSString *recipientId in self.recipientStateMap) {
TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId];
if (recipientState.deliveryTimestamp != nil) {
[result addObject:recipientId];
}
}
return result;
}
- (NSArray<NSString *> *)readRecipientIds
{
NSMutableArray<NSString *> *result = [NSMutableArray new];
for (NSString *recipientId in self.recipientStateMap) {
TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId];
if (recipientState.readTimestamp != nil) {
[result addObject:recipientId];
}
}
return result;
}
- (NSUInteger)sentRecipientsCount
{
return [self.recipientStateMap.allValues
filteredArrayUsingPredicate:[NSPredicate
predicateWithBlock:^BOOL(TSOutgoingMessageRecipientState *recipientState,
NSDictionary<NSString *, id> *_Nullable bindings) {
return recipientState.state == OWSOutgoingMessageRecipientStateSent;
}]]
.count;
}
- (nullable TSOutgoingMessageRecipientState *)recipientStateForRecipientId:(NSString *)recipientId
{
OWSAssert(recipientId.length > 0);
TSOutgoingMessageRecipientState *_Nullable result = self.recipientStateMap[recipientId];
OWSAssert(result);
2018-04-25 16:17:04 +02:00
return [result copy];
2018-04-23 16:30:51 +02:00
}
2017-11-15 19:15:48 +01:00
#pragma mark - Update With... Methods
- (void)updateWithSendingError:(NSError *)error
{
OWSAssert(error);
[self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
2017-11-15 19:15:48 +01:00
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
2018-04-23 16:30:51 +02:00
// Mark any "sending" recipients as "failed."
for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap
.allValues) {
if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
recipientState.state = OWSOutgoingMessageRecipientStateFailed;
}
}
2017-11-15 19:15:48 +01:00
[message setMostRecentFailureText:error.localizedDescription];
}];
}];
}
2018-04-23 16:30:51 +02:00
- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:(YapDatabaseReadWriteTransaction *)transaction
{
2018-04-23 16:30:51 +02:00
OWSAssert(transaction);
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
// Mark any "sending" recipients as "failed."
for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap
.allValues) {
if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
recipientState.state = OWSOutgoingMessageRecipientStateFailed;
}
}
}];
}
2018-04-23 16:30:51 +02:00
- (void)updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(transaction);
2017-11-15 19:15:48 +01:00
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
2018-04-23 16:30:51 +02:00
// Mark any "sending" recipients as "failed."
for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap
.allValues) {
if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) {
recipientState.state = OWSOutgoingMessageRecipientStateSending;
}
}
2017-11-15 19:15:48 +01:00
}];
}
- (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript
2017-11-14 19:41:01 +01:00
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
2017-11-15 19:15:48 +01:00
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
[message setHasSyncedTranscript:hasSyncedTranscript];
}];
}
- (void)updateWithCustomMessage:(NSString *)customMessage transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(customMessage);
OWSAssert(transaction);
2017-11-15 19:15:48 +01:00
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
[message setCustomMessage:customMessage];
}];
}
- (void)updateWithCustomMessage:(NSString *)customMessage
{
[self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self updateWithCustomMessage:customMessage transaction:transaction];
}];
}
2018-04-23 16:30:51 +02:00
- (void)updateWithSentRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(recipientId.length > 0);
OWSAssert(transaction);
2017-11-15 19:15:48 +01:00
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
2018-04-23 16:30:51 +02:00
TSOutgoingMessageRecipientState *_Nullable recipientState
= message.recipientStateMap[recipientId];
if (!recipientState) {
OWSFailDebug(@"Missing recipient state for recipient: %@", recipientId);
2018-04-23 16:30:51 +02:00
return;
2017-11-15 19:15:48 +01:00
}
2018-04-23 16:30:51 +02:00
recipientState.state = OWSOutgoingMessageRecipientStateSent;
2017-11-15 19:15:48 +01:00
}];
}
2018-04-23 16:30:51 +02:00
- (void)updateWithSkippedRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction
{
2018-04-23 16:30:51 +02:00
OWSAssert(recipientId.length > 0);
OWSAssert(transaction);
2017-11-15 19:15:48 +01:00
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
2018-04-23 16:30:51 +02:00
TSOutgoingMessageRecipientState *_Nullable recipientState
= message.recipientStateMap[recipientId];
if (!recipientState) {
OWSFailDebug(@"Missing recipient state for recipient: %@", recipientId);
2018-04-23 16:30:51 +02:00
return;
}
recipientState.state = OWSOutgoingMessageRecipientStateSkipped;
2017-11-15 19:15:48 +01:00
}];
}
2018-04-23 16:30:51 +02:00
- (void)updateWithDeliveredRecipient:(NSString *)recipientId
deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
2018-04-23 16:30:51 +02:00
OWSAssert(recipientId.length > 0);
OWSAssert(transaction);
// If delivery notification doesn't include timestamp, use "now" as an estimate.
if (!deliveryTimestamp) {
deliveryTimestamp = @([NSDate ows_millisecondTimeStamp]);
}
2017-11-15 19:15:48 +01:00
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
2018-04-23 16:30:51 +02:00
TSOutgoingMessageRecipientState *_Nullable recipientState
= message.recipientStateMap[recipientId];
if (!recipientState) {
OWSFailDebug(@"Missing recipient state for delivered recipient: %@", recipientId);
2018-04-23 16:30:51 +02:00
return;
}
2018-04-24 22:38:35 +02:00
if (recipientState.state != OWSOutgoingMessageRecipientStateSent) {
OWSLogWarn(@"marking unsent message as delivered.");
2018-04-24 22:38:35 +02:00
}
2018-04-23 16:30:51 +02:00
recipientState.state = OWSOutgoingMessageRecipientStateSent;
recipientState.deliveryTimestamp = deliveryTimestamp;
2017-11-15 19:15:48 +01:00
}];
}
2018-04-23 16:30:51 +02:00
- (void)updateWithReadRecipientId:(NSString *)recipientId
readTimestamp:(uint64_t)readTimestamp
transaction:(YapDatabaseReadWriteTransaction *)transaction
2017-04-17 22:45:22 +02:00
{
2018-04-23 16:30:51 +02:00
OWSAssert(recipientId.length > 0);
OWSAssert(transaction);
2017-04-17 22:45:22 +02:00
2018-04-23 16:30:51 +02:00
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
TSOutgoingMessageRecipientState *_Nullable recipientState
= message.recipientStateMap[recipientId];
if (!recipientState) {
OWSFailDebug(@"Missing recipient state for delivered recipient: %@", recipientId);
2018-04-23 16:30:51 +02:00
return;
}
2018-04-24 22:38:35 +02:00
if (recipientState.state != OWSOutgoingMessageRecipientStateSent) {
OWSLogWarn(@"marking unsent message as delivered.");
2018-04-24 22:38:35 +02:00
}
2018-04-23 16:30:51 +02:00
recipientState.state = OWSOutgoingMessageRecipientStateSent;
recipientState.readTimestamp = @(readTimestamp);
}];
2017-04-17 22:45:22 +02:00
}
2018-04-23 16:30:51 +02:00
- (void)updateWithWasSentFromLinkedDeviceWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(transaction);
2018-04-23 16:30:51 +02:00
2017-11-15 19:15:48 +01:00
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
2018-04-23 16:30:51 +02:00
// Mark any "sending" recipients as "sent."
for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap
.allValues) {
if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
recipientState.state = OWSOutgoingMessageRecipientStateSent;
}
}
[message setIsFromLinkedDevice:YES];
2017-11-15 19:15:48 +01:00
}];
}
2018-04-24 22:38:35 +02:00
- (void)updateWithSendingToSingleGroupRecipient:(NSString *)singleGroupRecipient
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(transaction);
2018-04-23 16:30:51 +02:00
OWSAssert(singleGroupRecipient.length > 0);
2017-11-15 19:15:48 +01:00
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
2018-04-23 16:30:51 +02:00
TSOutgoingMessageRecipientState *recipientState =
[TSOutgoingMessageRecipientState new];
recipientState.state = OWSOutgoingMessageRecipientStateSending;
[message setRecipientStateMap:@{
singleGroupRecipient : recipientState,
}];
2017-11-15 19:15:48 +01:00
}];
}
- (nullable NSNumber *)firstRecipientReadTimestamp
{
NSNumber *result = nil;
2018-04-23 16:30:51 +02:00
for (TSOutgoingMessageRecipientState *recipientState in self.recipientStateMap.allValues) {
if (!recipientState.readTimestamp) {
continue;
}
if (!result || (result.unsignedLongLongValue > recipientState.readTimestamp.unsignedLongLongValue)) {
result = recipientState.readTimestamp;
}
}
return result;
}
2018-04-23 16:30:51 +02:00
- (void)updateWithFakeMessageState:(TSOutgoingMessageState)messageState
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(transaction);
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap
.allValues) {
switch (messageState) {
case TSOutgoingMessageStateSending:
recipientState.state = OWSOutgoingMessageRecipientStateSending;
break;
case TSOutgoingMessageStateFailed:
recipientState.state = OWSOutgoingMessageRecipientStateFailed;
break;
case TSOutgoingMessageStateSent:
recipientState.state = OWSOutgoingMessageRecipientStateSent;
break;
default:
OWSFailDebug(@"unexpected message state.");
2018-04-23 16:30:51 +02:00
break;
}
}
}];
}
#pragma mark -
- (nullable SSKProtoDataMessageBuilder *)dataMessageBuilder
{
TSThread *thread = self.thread;
OWSAssert(thread);
SSKProtoDataMessageBuilder *builder = [SSKProtoDataMessageBuilder new];
[builder setTimestamp:self.timestamp];
2018-04-10 18:59:26 +02:00
if ([self.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold) {
[builder setBody:self.body];
} else {
OWSFailDebug(@"message body length too long.");
2018-04-23 16:30:51 +02:00
NSString *truncatedBody = [self.body copy];
2018-04-10 18:59:26 +02:00
while ([truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kOversizeTextMessageSizeThreshold) {
OWSLogError(@"truncating body which is too long: %lu",
2018-05-07 18:32:31 +02:00
(unsigned long)[truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
2018-04-10 18:59:26 +02:00
truncatedBody = [truncatedBody substringToIndex:truncatedBody.length / 2];
}
[builder setBody:truncatedBody];
}
[builder setExpireTimer:self.expiresInSeconds];
// Group Messages
2016-08-29 00:31:27 +02:00
BOOL attachmentWasGroupAvatar = NO;
if ([thread isKindOfClass:[TSGroupThread class]]) {
TSGroupThread *gThread = (TSGroupThread *)thread;
SSKProtoGroupContextBuilder *groupBuilder = [SSKProtoGroupContextBuilder new];
switch (self.groupMetaMessage) {
case TSGroupMessageQuit:
[groupBuilder setType:SSKProtoGroupContextTypeQuit];
break;
case TSGroupMessageUpdate:
case TSGroupMessageNew: {
2016-08-29 00:31:27 +02:00
if (gThread.groupModel.groupImage != nil && self.attachmentIds.count == 1) {
attachmentWasGroupAvatar = YES;
SSKProtoAttachmentPointer *_Nullable attachmentProto =
[TSAttachmentStream buildProtoForAttachmentId:self.attachmentIds.firstObject];
if (!attachmentProto) {
OWSFailDebug(@"could not build protobuf.");
return nil;
}
[groupBuilder setAvatar:attachmentProto];
}
2016-08-29 00:31:27 +02:00
[groupBuilder setMembers:gThread.groupModel.groupMemberIds];
[groupBuilder setName:gThread.groupModel.groupName];
[groupBuilder setType:SSKProtoGroupContextTypeUpdate];
break;
}
default:
[groupBuilder setType:SSKProtoGroupContextTypeDeliver];
break;
}
[groupBuilder setId:gThread.groupModel.groupId];
NSError *error;
SSKProtoGroupContext *_Nullable groupContextProto = [groupBuilder buildAndReturnError:&error];
if (error || !groupContextProto) {
OWSFailDebug(@"could not build protobuf: %@.", error);
return nil;
}
[builder setGroup:groupContextProto];
}
// Message Attachments
2016-08-29 00:31:27 +02:00
if (!attachmentWasGroupAvatar) {
NSMutableArray *attachments = [NSMutableArray new];
for (NSString *attachmentId in self.attachmentIds) {
SSKProtoAttachmentPointer *_Nullable attachmentProto =
[TSAttachmentStream buildProtoForAttachmentId:attachmentId];
if (!attachmentProto) {
OWSFailDebug(@"could not build protobuf.");
return nil;
}
[attachments addObject:attachmentProto];
}
[builder setAttachments:attachments];
}
2018-04-30 22:42:14 +02:00
// Quoted Reply
SSKProtoDataMessageQuoteBuilder *_Nullable quotedMessageBuilder = self.quotedMessageBuilder;
2018-04-30 22:42:14 +02:00
if (quotedMessageBuilder) {
NSError *error;
SSKProtoDataMessageQuote *_Nullable quoteProto = [quotedMessageBuilder buildAndReturnError:&error];
if (error || !quoteProto) {
OWSFailDebug(@"could not build protobuf: %@.", error);
return nil;
}
[builder setQuote:quoteProto];
2018-04-30 22:42:14 +02:00
}
// Contact Share
if (self.contactShare) {
SSKProtoDataMessageContact *_Nullable contactProto =
[OWSContacts protoForContact:self.contactShare];
if (contactProto) {
[builder addContact:contactProto];
} else {
OWSFailDebug(@"contactProto was unexpectedly nil");
}
2018-04-30 22:42:14 +02:00
}
return builder;
}
- (nullable SSKProtoDataMessageQuoteBuilder *)quotedMessageBuilder
2018-04-30 22:42:14 +02:00
{
if (!self.quotedMessage) {
return nil;
}
TSQuotedMessage *quotedMessage = self.quotedMessage;
SSKProtoDataMessageQuoteBuilder *quoteBuilder = [SSKProtoDataMessageQuoteBuilder new];
2018-04-30 22:42:14 +02:00
[quoteBuilder setId:quotedMessage.timestamp];
[quoteBuilder setAuthor:quotedMessage.authorId];
2018-04-30 22:42:14 +02:00
BOOL hasQuotedText = NO;
BOOL hasQuotedAttachment = NO;
if (self.quotedMessage.body.length > 0) {
hasQuotedText = YES;
[quoteBuilder setText:quotedMessage.body];
}
if (quotedMessage.quotedAttachments) {
for (OWSAttachmentInfo *attachment in quotedMessage.quotedAttachments) {
hasQuotedAttachment = YES;
SSKProtoDataMessageQuoteQuotedAttachmentBuilder *quotedAttachmentBuilder =
[SSKProtoDataMessageQuoteQuotedAttachmentBuilder new];
2018-04-30 22:42:14 +02:00
quotedAttachmentBuilder.contentType = attachment.contentType;
quotedAttachmentBuilder.fileName = attachment.sourceFilename;
if (attachment.thumbnailAttachmentStreamId) {
quotedAttachmentBuilder.thumbnail =
[TSAttachmentStream buildProtoForAttachmentId:attachment.thumbnailAttachmentStreamId];
}
2018-04-30 22:42:14 +02:00
NSError *error;
SSKProtoDataMessageQuoteQuotedAttachment *_Nullable quotedAttachmentMessage =
[quotedAttachmentBuilder buildAndReturnError:&error];
2018-08-03 20:52:55 +02:00
if (error || !quotedAttachmentMessage) {
OWSFailDebug(@"could not build protobuf: %@", error);
return nil;
}
[quoteBuilder addAttachments:quotedAttachmentMessage];
}
2018-04-30 22:42:14 +02:00
}
if (hasQuotedText || hasQuotedAttachment) {
return quoteBuilder;
} else {
OWSFailDebug(@"Invalid quoted message data.");
2018-04-30 22:42:14 +02:00
return nil;
}
}
// recipientId is nil when building "sent" sync messages for messages sent to groups.
- (nullable SSKProtoDataMessage *)buildDataMessage:(NSString *_Nullable)recipientId
{
OWSAssert(self.thread);
SSKProtoDataMessageBuilder *_Nullable builder = [self dataMessageBuilder];
if (!builder) {
OWSFailDebug(@"could not build protobuf.");
return nil;
}
[ProtoUtils addLocalProfileKeyIfNecessary:self.thread recipientId:recipientId dataMessageBuilder:builder];
2018-05-15 22:10:42 +02:00
NSError *error;
SSKProtoDataMessage *_Nullable dataProto = [builder buildAndReturnError:&error];
if (error || !dataProto) {
OWSFailDebug(@"could not build protobuf: %@", error);
return nil;
}
return dataProto;
}
- (nullable NSData *)buildPlainTextData:(SignalRecipient *)recipient
{
NSError *error;
SSKProtoDataMessage *_Nullable dataMessage = [self buildDataMessage:recipient.recipientId];
2018-08-03 20:52:55 +02:00
if (error || !dataMessage) {
OWSFailDebug(@"could not build protobuf: %@", error);
return nil;
}
SSKProtoContentBuilder *contentBuilder = [SSKProtoContentBuilder new];
[contentBuilder setDataMessage:dataMessage];
2018-08-03 20:52:55 +02:00
NSData *_Nullable contentData = [contentBuilder buildSerializedDataAndReturnError:&error];
if (error || !contentData) {
OWSFailDebug(@"could not serialize protobuf: %@", error);
return nil;
}
return contentData;
}
- (BOOL)shouldSyncTranscript
{
return !self.hasSyncedTranscript;
}
2018-04-30 16:21:58 +02:00
- (NSString *)statusDescription
{
NSMutableString *result = [NSMutableString new];
[result appendFormat:@"[status: %@", NSStringForOutgoingMessageState(self.messageState)];
for (NSString *recipientId in self.recipientStateMap) {
TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId];
[result appendFormat:@", %@: %@", recipientId, NSStringForOutgoingMessageRecipientState(recipientState.state)];
}
[result appendString:@"]"];
return [result copy];
}
2015-12-07 03:31:43 +01:00
@end
NS_ASSUME_NONNULL_END