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

570 lines
23 KiB
Objective-C

//
// Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
#import "TSOutgoingMessage.h"
#import "TSQuotedMessage.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
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";
}
}
#pragma mark -
@interface TSMessage (Private)
- (void)removeAllAttachmentsWithTransaction:(DBWriteTransaction *)transaction;
@end
#pragma mark -
NSUInteger const TSOutgoingMessageSchemaVersion = 1;
@interface TSOutgoingMessage ()
@property (atomic) BOOL hasSyncedTranscript;
@property (atomic, nullable) NSString *customMessage;
@property (atomic) NSInteger 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
body:(nullable NSString *)body
bodyRanges:(nullable MessageBodyRanges *)bodyRanges
contactShare:(nullable OWSContact *)contactShare
deprecated_attachmentIds:(nullable NSArray<NSString *> *)deprecated_attachmentIds
editState:(TSEditState)editState
expireStartedAt:(uint64_t)expireStartedAt
expireTimerVersion:(nullable NSNumber *)expireTimerVersion
expiresAt:(uint64_t)expiresAt
expiresInSeconds:(unsigned int)expiresInSeconds
giftBadge:(nullable OWSGiftBadge *)giftBadge
isGroupStoryReply:(BOOL)isGroupStoryReply
isPoll:(BOOL)isPoll
isSmsMessageRestoredFromBackup:(BOOL)isSmsMessageRestoredFromBackup
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:(NSInteger)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
body:body
bodyRanges:bodyRanges
contactShare:contactShare
deprecated_attachmentIds:deprecated_attachmentIds
editState:editState
expireStartedAt:expireStartedAt
expireTimerVersion:expireTimerVersion
expiresAt:expiresAt
expiresInSeconds:expiresInSeconds
giftBadge:giftBadge
isGroupStoryReply:isGroupStoryReply
isPoll:isPoll
isSmsMessageRestoredFromBackup:isSmsMessageRestoredFromBackup
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
+ (BOOL)supportsSecureCoding
{
return YES;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[super encodeWithCoder:coder];
NSData *changeActionsProtoData = self.changeActionsProtoData;
if (changeActionsProtoData != nil) {
[coder encodeObject:changeActionsProtoData forKey:@"changeActionsProtoData"];
}
NSString *customMessage = self.customMessage;
if (customMessage != nil) {
[coder encodeObject:customMessage forKey:@"customMessage"];
}
[coder encodeObject:[self valueForKey:@"groupMetaMessage"] forKey:@"groupMetaMessage"];
[coder encodeObject:[self valueForKey:@"hasLegacyMessageState"] forKey:@"hasLegacyMessageState"];
[coder encodeObject:[self valueForKey:@"hasSyncedTranscript"] forKey:@"hasSyncedTranscript"];
[coder encodeObject:[self valueForKey:@"isVoiceMessage"] forKey:@"isVoiceMessage"];
[coder encodeObject:[self valueForKey:@"legacyMessageState"] forKey:@"legacyMessageState"];
[coder encodeObject:[self valueForKey:@"legacyWasDelivered"] forKey:@"legacyWasDelivered"];
NSString *mostRecentFailureText = self.mostRecentFailureText;
if (mostRecentFailureText != nil) {
[coder encodeObject:mostRecentFailureText forKey:@"mostRecentFailureText"];
}
[coder encodeObject:[self valueForKey:@"outgoingMessageSchemaVersion"] forKey:@"outgoingMessageSchemaVersion"];
NSDictionary *recipientAddressStates = self.recipientAddressStates;
if (recipientAddressStates != nil) {
[coder encodeObject:recipientAddressStates forKey:@"recipientAddressStates"];
}
[coder encodeObject:[self valueForKey:@"storedMessageState"] forKey:@"storedMessageState"];
[coder encodeObject:[self valueForKey:@"wasNotCreatedLocally"] forKey:@"wasNotCreatedLocally"];
}
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (!self) {
return self;
}
self->_changeActionsProtoData = [coder decodeObjectOfClass:[NSData class] forKey:@"changeActionsProtoData"];
self->_customMessage = [coder decodeObjectOfClass:[NSString class] forKey:@"customMessage"];
self->_groupMetaMessage = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
forKey:@"groupMetaMessage"] integerValue];
self->_hasLegacyMessageState = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
forKey:@"hasLegacyMessageState"] boolValue];
self->_hasSyncedTranscript = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
forKey:@"hasSyncedTranscript"] boolValue];
self->_isVoiceMessage = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
forKey:@"isVoiceMessage"] boolValue];
self->_legacyMessageState = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
forKey:@"legacyMessageState"] integerValue];
self->_legacyWasDelivered = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
forKey:@"legacyWasDelivered"] boolValue];
self->_mostRecentFailureText = [coder decodeObjectOfClass:[NSString class] forKey:@"mostRecentFailureText"];
self->_outgoingMessageSchemaVersion =
[(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
forKey:@"outgoingMessageSchemaVersion"] unsignedIntegerValue];
self->_recipientAddressStates = [coder decodeObjectOfClasses:[NSSet setWithArray:@[
[NSDictionary class],
[SignalServiceAddress class],
[TSOutgoingMessageRecipientState class]
]]
forKey:@"recipientAddressStates"];
self->_storedMessageState = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
forKey:@"storedMessageState"] integerValue];
self->_wasNotCreatedLocally = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
forKey:@"wasNotCreatedLocally"] boolValue];
#ifndef TESTABLE_BUILD
OWSAssertDebug(self.outgoingMessageSchemaVersion >= 1);
#endif
_outgoingMessageSchemaVersion = TSOutgoingMessageSchemaVersion;
return self;
}
- (NSUInteger)hash
{
NSUInteger result = [super hash];
result ^= self.changeActionsProtoData.hash;
result ^= self.customMessage.hash;
result ^= (NSUInteger)self.groupMetaMessage;
result ^= self.hasLegacyMessageState;
result ^= self.hasSyncedTranscript;
result ^= self.isVoiceMessage;
result ^= (NSUInteger)self.legacyMessageState;
result ^= self.legacyWasDelivered;
result ^= self.mostRecentFailureText.hash;
result ^= self.outgoingMessageSchemaVersion;
result ^= self.recipientAddressStates.hash;
result ^= (NSUInteger)self.storedMessageState;
result ^= self.wasNotCreatedLocally;
return result;
}
- (BOOL)isEqual:(id)other
{
if (![super isEqual:other]) {
return NO;
}
TSOutgoingMessage *typedOther = (TSOutgoingMessage *)other;
if (![NSObject isObject:self.changeActionsProtoData equalToObject:typedOther.changeActionsProtoData]) {
return NO;
}
if (![NSObject isObject:self.customMessage equalToObject:typedOther.customMessage]) {
return NO;
}
if (self.groupMetaMessage != typedOther.groupMetaMessage) {
return NO;
}
if (self.hasLegacyMessageState != typedOther.hasLegacyMessageState) {
return NO;
}
if (self.hasSyncedTranscript != typedOther.hasSyncedTranscript) {
return NO;
}
if (self.isVoiceMessage != typedOther.isVoiceMessage) {
return NO;
}
if (self.legacyMessageState != typedOther.legacyMessageState) {
return NO;
}
if (self.legacyWasDelivered != typedOther.legacyWasDelivered) {
return NO;
}
if (![NSObject isObject:self.mostRecentFailureText equalToObject:typedOther.mostRecentFailureText]) {
return NO;
}
if (self.outgoingMessageSchemaVersion != typedOther.outgoingMessageSchemaVersion) {
return NO;
}
if (![NSObject isObject:self.recipientAddressStates equalToObject:typedOther.recipientAddressStates]) {
return NO;
}
if (self.storedMessageState != typedOther.storedMessageState) {
return NO;
}
if (self.wasNotCreatedLocally != typedOther.wasNotCreatedLocally) {
return NO;
}
return YES;
}
- (instancetype)initOutgoingMessageWithBuilder:(TSOutgoingMessageBuilder *)outgoingMessageBuilder
additionalRecipients:(NSArray<ServiceIdObjC *> *)additionalRecipients
explicitRecipients:(NSArray<AciObjC *> *)explicitRecipients
skippedRecipients:(NSArray<ServiceIdObjC *> *)skippedRecipients
transaction:(DBReadTransaction *)transaction
{
self = [super initMessageWithBuilder:outgoingMessageBuilder];
if (!self) {
return self;
}
TSThread *thread = outgoingMessageBuilder.thread;
// New outgoing messages should immediately determine their
// recipient list from current thread state.
NSMutableSet<SignalServiceAddress *> *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 recipientAddressesWith:transaction]];
// Some messages (eg certain call messages) go to a subset of the group.
if (explicitRecipients.count > 0) {
NSMutableSet<SignalServiceAddress *> *explicitRecipientAddresses = [[NSMutableSet alloc] init];
for (AciObjC *recipientAci in explicitRecipients) {
[explicitRecipientAddresses
addObject:[[SignalServiceAddress alloc] initWithServiceIdObjC:recipientAci]];
}
[recipientAddresses intersectSet:explicitRecipientAddresses];
}
// Group updates should also be sent to pending members of the group.
if (additionalRecipients.count > 0) {
for (ServiceIdObjC *serviceId in additionalRecipients) {
[recipientAddresses addObject:[[SignalServiceAddress alloc] initWithServiceIdObjC:serviceId]];
}
}
}
NSSet<ServiceIdObjC *> *skippedRecipientsSet = [NSSet setWithArray:skippedRecipients];
NSMutableDictionary<SignalServiceAddress *, TSOutgoingMessageRecipientState *> *recipientAddressStates =
[NSMutableDictionary new];
for (SignalServiceAddress *recipientAddress in recipientAddresses) {
if (!recipientAddress.isValid) {
OWSFailDebug(@"Ignoring invalid address.");
continue;
}
ServiceIdObjC *serviceId = recipientAddress.serviceIdObjC;
OWSOutgoingMessageRecipientStatus recipientStatus
= serviceId != nil && [skippedRecipientsSet containsObject:serviceId]
? OWSOutgoingMessageRecipientStatusSkipped
: OWSOutgoingMessageRecipientStatusSending;
TSOutgoingMessageRecipientState *recipientState =
[[TSOutgoingMessageRecipientState alloc] initWithStatus:recipientStatus];
recipientAddressStates[recipientAddress] = recipientState;
}
_recipientAddressStates = [recipientAddressStates copy];
_hasSyncedTranscript = NO;
_outgoingMessageSchemaVersion = TSOutgoingMessageSchemaVersion;
_changeActionsProtoData = outgoingMessageBuilder.groupChangeProtoData;
_isVoiceMessage = outgoingMessageBuilder.isVoiceMessage;
_wasNotCreatedLocally = outgoingMessageBuilder.wasNotCreatedLocally;
return self;
}
- (instancetype)initOutgoingMessageWithBuilder:(TSOutgoingMessageBuilder *)outgoingMessageBuilder
recipientAddressStates:
(NSDictionary<SignalServiceAddress *, TSOutgoingMessageRecipientState *> *)
recipientAddressStates
{
self = [super initMessageWithBuilder:outgoingMessageBuilder];
if (!self) {
return self;
}
_recipientAddressStates = [recipientAddressStates copy];
_hasSyncedTranscript = NO;
_outgoingMessageSchemaVersion = TSOutgoingMessageSchemaVersion;
_changeActionsProtoData = outgoingMessageBuilder.groupChangeProtoData;
_isVoiceMessage = outgoingMessageBuilder.isVoiceMessage;
_wasNotCreatedLocally = outgoingMessageBuilder.wasNotCreatedLocally;
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);
}
- (void)updateStoredMessageState
{
_storedMessageState = self.messageState;
}
- (void)anyWillInsertWithTransaction:(DBWriteTransaction *)transaction
{
[super anyWillInsertWithTransaction:transaction];
[self updateStoredMessageState];
}
- (void)anyDidInsertWithTransaction:(DBWriteTransaction *)transaction
{
[super anyDidInsertWithTransaction:transaction];
[self markMessageSendLogEntryCompleteIfNeededWithTx:transaction];
}
- (void)anyWillUpdateWithTransaction:(DBWriteTransaction *)transaction
{
[super anyWillUpdateWithTransaction:transaction];
[self updateStoredMessageState];
}
- (void)anyDidUpdateWithTransaction:(DBWriteTransaction *)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;
}
return [TSOutgoingMessage isEligibleToStartExpireTimerWithMessageState:self.messageState];
}
- (BOOL)isOnline
{
return NO;
}
- (BOOL)isUrgent
{
return YES;
}
- (OWSInteractionType)interactionType
{
return OWSInteractionType_OutgoingMessage;
}
#pragma mark - Update With... Methods
- (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript transaction:(DBWriteTransaction *)transaction
{
[self anyUpdateOutgoingMessageWithTransaction:transaction
block:^(TSOutgoingMessage *message) {
[message setHasSyncedTranscript:hasSyncedTranscript];
}];
}
#pragma mark -
- (nullable SSKProtoDataMessageBuilder *)dataMessageBuilderWithThread:(TSThread *)thread
transaction:(DBReadTransaction *)transaction
{
return [self _dataMessageBuilderWithThread:thread tx:transaction];
}
// recipientId is nil when building "sent" sync messages for messages sent to groups.
- (nullable SSKProtoDataMessage *)buildDataMessage:(TSThread *)thread transaction:(DBReadTransaction *)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:(DBReadTransaction *)transaction
{
SSKProtoDataMessage *_Nullable dataMessage = [self buildDataMessage:thread transaction:transaction];
if (!dataMessage) {
return nil;
}
SSKProtoContentBuilder *contentBuilder = [SSKProtoContent builder];
[contentBuilder setDataMessage:dataMessage];
return contentBuilder;
}
- (nullable NSData *)buildPlaintextDataInThread:(TSThread *)thread
tx:(DBWriteTransaction *)transaction
error:(NSError **)error
{
return [self _buildPlaintextDataInThread:thread tx:transaction error:error];
}
- (BOOL)shouldSyncTranscript
{
return YES;
}
- (nullable OWSOutgoingSyncMessage *)buildSyncTranscriptMessageWithLocalThread:(TSContactThread *)localThread
transaction:(DBWriteTransaction *)transaction
error:(NSError **)error
{
return [self _buildSyncTranscriptMessageWithLocalThread:localThread tx:transaction error:error];
}
@end
NS_ASSUME_NONNULL_END