Signal-iOS/SignalServiceKit/Messages/OWSReceiptManager.m
2024-03-27 12:22:52 -05:00

292 lines
11 KiB
Objective-C

//
// Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
#import "OWSReceiptManager.h"
#import "AppReadiness.h"
#import "OWSLinkedDeviceReadReceipt.h"
#import "OWSReadReceiptsForLinkedDevicesMessage.h"
#import "OWSReceiptsForSenderMessage.h"
#import "TSContactThread.h"
#import "TSIncomingMessage.h"
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalCoreKit/Threading.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
#pragma mark -
NSString *const OWSReceiptManagerCollection = @"OWSReadReceiptManagerCollection";
NSString *const OWSReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsEnabled";
@interface OWSReceiptManager ()
// Should only be accessed while synchronized on the OWSReceiptManager.
@property (nonatomic) BOOL isProcessing;
@property (atomic, nullable) NSNumber *areReadReceiptsEnabledCached;
@end
#pragma mark -
@implementation OWSReceiptManager
+ (SDSKeyValueStore *)keyValueStore
{
static SDSKeyValueStore *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(
&onceToken, ^{ instance = [[SDSKeyValueStore alloc] initWithCollection:OWSReceiptManagerCollection]; });
return instance;
}
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
OWSSingletonAssert();
// Start processing.
AppReadinessRunNowOrWhenAppDidBecomeReadyAsync(^{ [self scheduleProcessing]; });
return self;
}
// Schedules a processing pass, unless one is already scheduled.
- (void)scheduleProcessing
{
OWSAssertDebug(AppReadiness.isAppReady);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(self) {
if (self.isProcessing) {
return;
}
self.isProcessing = YES;
}
[self processReceiptsForLinkedDevicesWithCompletion:^{
@synchronized(self) {
OWSAssertDebug(self.isProcessing);
self.isProcessing = NO;
}
}];
});
}
- (void)messageWasRead:(TSIncomingMessage *)message
thread:(TSThread *)thread
circumstance:(OWSReceiptCircumstance)circumstance
transaction:(SDSAnyWriteTransaction *)transaction
{
switch (circumstance) {
case OWSReceiptCircumstanceOnLinkedDevice:
// nothing further to do
return;
case OWSReceiptCircumstanceOnLinkedDeviceWhilePendingMessageRequest:
if ([self areReadReceiptsEnabled]) {
[self.pendingReceiptRecorder recordPendingReadReceiptForMessage:message
thread:thread
transaction:transaction.unwrapGrdbWrite];
}
break;
case OWSReceiptCircumstanceOnThisDevice: {
[self enqueueLinkedDeviceReadReceiptForMessage:message transaction:transaction];
[transaction addAsyncCompletionOffMain:^{ [self scheduleProcessing]; }];
if (message.authorAddress.isLocalAddress) {
OWSFailDebug(@"We don't support incoming messages from self.");
return;
}
if ([self areReadReceiptsEnabled]) {
ReceiptSender *receiptSender = SSKEnvironment.shared.receiptSenderRef;
[receiptSender enqueueReadReceiptFor:message.authorAddress
timestamp:message.timestamp
messageUniqueId:message.uniqueId
tx:transaction];
}
break;
}
case OWSReceiptCircumstanceOnThisDeviceWhilePendingMessageRequest:
[self enqueueLinkedDeviceReadReceiptForMessage:message transaction:transaction];
if ([self areReadReceiptsEnabled]) {
[self.pendingReceiptRecorder recordPendingReadReceiptForMessage:message
thread:thread
transaction:transaction.unwrapGrdbWrite];
}
break;
}
}
- (void)messageWasViewed:(TSIncomingMessage *)message
thread:(TSThread *)thread
circumstance:(OWSReceiptCircumstance)circumstance
transaction:(SDSAnyWriteTransaction *)transaction
{
switch (circumstance) {
case OWSReceiptCircumstanceOnLinkedDevice:
// nothing further to do
return;
case OWSReceiptCircumstanceOnLinkedDeviceWhilePendingMessageRequest:
if ([self areReadReceiptsEnabled]) {
[self.pendingReceiptRecorder recordPendingViewedReceiptForMessage:message
thread:thread
transaction:transaction.unwrapGrdbWrite];
}
break;
case OWSReceiptCircumstanceOnThisDevice: {
[self enqueueLinkedDeviceViewedReceiptForIncomingMessage:message transaction:transaction];
[transaction addAsyncCompletionOffMain:^{ [self scheduleProcessing]; }];
if (message.authorAddress.isLocalAddress) {
OWSFailDebug(@"We don't support incoming messages from self.");
return;
}
if ([self areReadReceiptsEnabled]) {
ReceiptSender *receiptSender = SSKEnvironment.shared.receiptSenderRef;
[receiptSender enqueueViewedReceiptFor:message.authorAddress
timestamp:message.timestamp
messageUniqueId:message.uniqueId
tx:transaction];
}
break;
}
case OWSReceiptCircumstanceOnThisDeviceWhilePendingMessageRequest:
[self enqueueLinkedDeviceViewedReceiptForIncomingMessage:message transaction:transaction];
if ([self areReadReceiptsEnabled]) {
[self.pendingReceiptRecorder recordPendingViewedReceiptForMessage:message
thread:thread
transaction:transaction.unwrapGrdbWrite];
}
break;
}
}
- (void)storyWasRead:(StoryMessage *)storyMessage
circumstance:(OWSReceiptCircumstance)circumstance
transaction:(SDSAnyWriteTransaction *)transaction
{
switch (circumstance) {
case OWSReceiptCircumstanceOnLinkedDevice:
// nothing further to do
break;
case OWSReceiptCircumstanceOnLinkedDeviceWhilePendingMessageRequest:
OWSFailDebug(@"Unexpectedly had story receipt blocked by message request.");
break;
case OWSReceiptCircumstanceOnThisDevice: {
// We only send read receipts to linked devices, not to the author.
[self enqueueLinkedDeviceReadReceiptForStoryMessage:storyMessage transaction:transaction];
[transaction addAsyncCompletionOffMain:^{ [self scheduleProcessing]; }];
break;
}
case OWSReceiptCircumstanceOnThisDeviceWhilePendingMessageRequest:
OWSFailDebug(@"Unexpectedly had story receipt blocked by message request.");
break;
}
}
- (void)storyWasViewed:(StoryMessage *)storyMessage
circumstance:(OWSReceiptCircumstance)circumstance
transaction:(SDSAnyWriteTransaction *)transaction
{
switch (circumstance) {
case OWSReceiptCircumstanceOnLinkedDevice:
// nothing further to do
break;
case OWSReceiptCircumstanceOnLinkedDeviceWhilePendingMessageRequest:
OWSFailDebug(@"Unexpectedly had story receipt blocked by message request.");
break;
case OWSReceiptCircumstanceOnThisDevice: {
[self enqueueLinkedDeviceViewedReceiptForStoryMessage:storyMessage transaction:transaction];
[transaction addAsyncCompletionOffMain:^{ [self scheduleProcessing]; }];
if (StoryManager.areViewReceiptsEnabled) {
[self enqueueSenderViewedReceiptForStoryMessage:storyMessage transaction:transaction];
}
break;
}
case OWSReceiptCircumstanceOnThisDeviceWhilePendingMessageRequest:
OWSFailDebug(@"Unexpectedly had story receipt blocked by message request.");
break;
}
}
- (void)incomingGiftWasRedeemed:(TSIncomingMessage *)incomingMessage transaction:(SDSAnyWriteTransaction *)transaction
{
[self enqueueLinkedDeviceViewedReceiptForIncomingMessage:incomingMessage transaction:transaction];
[transaction addAsyncCompletionOffMain:^{ [self scheduleProcessing]; }];
}
- (void)outgoingGiftWasOpened:(TSOutgoingMessage *)outgoingMessage transaction:(SDSAnyWriteTransaction *)transaction
{
[self enqueueLinkedDeviceViewedReceiptForOutgoingMessage:outgoingMessage transaction:transaction];
[transaction addAsyncCompletionOffMain:^{ [self scheduleProcessing]; }];
}
#pragma mark - Settings
- (void)prepareCachedValues
{
// Clear out so we re-initialize if we ever re-run the "on launch" logic,
// such as after a completed database transfer.
self.areReadReceiptsEnabledCached = nil;
[self areReadReceiptsEnabled];
}
- (BOOL)areReadReceiptsEnabled
{
// We don't need to worry about races around this cached value.
if (self.areReadReceiptsEnabledCached == nil) {
[self.databaseStorage
readWithBlock:^(SDSAnyReadTransaction *transaction) {
self.areReadReceiptsEnabledCached = @([self areReadReceiptsEnabledWithTransaction:transaction]);
}
file:__FILE__
function:__FUNCTION__
line:__LINE__];
}
return [self.areReadReceiptsEnabledCached boolValue];
}
- (BOOL)areReadReceiptsEnabledWithTransaction:(SDSAnyReadTransaction *)transaction
{
return [OWSReceiptManager.keyValueStore getBool:OWSReceiptManagerAreReadReceiptsEnabled
defaultValue:NO
transaction:transaction];
}
- (void)setAreReadReceiptsEnabledWithSneakyTransactionAndSyncConfiguration:(BOOL)value
{
OWSLogInfo(@"setAreReadReceiptsEnabled: %d.", value);
DatabaseStorageWrite(self.databaseStorage,
^(SDSAnyWriteTransaction *transaction) { [self setAreReadReceiptsEnabled:value transaction:transaction]; });
[SSKEnvironment.shared.syncManager sendConfigurationSyncMessage];
[SSKEnvironment.shared.storageServiceManagerObjc recordPendingLocalAccountUpdates];
}
- (void)setAreReadReceiptsEnabled:(BOOL)value transaction:(SDSAnyWriteTransaction *)transaction
{
[OWSReceiptManager.keyValueStore setBool:value key:OWSReceiptManagerAreReadReceiptsEnabled transaction:transaction];
self.areReadReceiptsEnabledCached = @(value);
}
@end
NS_ASSUME_NONNULL_END