// // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "OWSMessageManager.h" #import "AppContext.h" #import "AppReadiness.h" #import "ContactsManagerProtocol.h" #import "MimeTypeUtil.h" #import "NSNotificationCenter+OWS.h" #import "NSString+SSK.h" #import "NotificationsProtocol.h" #import "OWSAttachmentDownloads.h" #import "OWSBlockingManager.h" #import "OWSCallMessageHandler.h" #import "OWSContact.h" #import "OWSDevice.h" #import "OWSDevicesService.h" #import "OWSDisappearingConfigurationUpdateInfoMessage.h" #import "OWSDisappearingMessagesConfiguration.h" #import "OWSDisappearingMessagesJob.h" #import "LKEphemeralMessage.h" #import "LKSessionRequestMessage.h" #import "LKDeviceLinkMessage.h" #import "OWSIdentityManager.h" #import "OWSIncomingMessageFinder.h" #import "OWSIncomingSentMessageTranscript.h" #import "OWSMessageSender.h" #import "OWSMessageUtils.h" #import "OWSOutgoingReceiptManager.h" #import "OWSPrimaryStorage+SessionStore.h" #import "OWSPrimaryStorage+Loki.h" #import "OWSPrimaryStorage.h" #import "OWSReadReceiptManager.h" #import "OWSRecordTranscriptJob.h" #import "OWSSyncGroupsMessage.h" #import "OWSSyncGroupsRequestMessage.h" #import "ProfileManagerProtocol.h" #import "SSKEnvironment.h" #import "TSAccountManager.h" #import "TSAttachment.h" #import "TSAttachmentPointer.h" #import "TSAttachmentStream.h" #import "TSContactThread.h" #import "TSDatabaseView.h" #import "TSGroupModel.h" #import "TSGroupThread.h" #import "TSIncomingMessage.h" #import "TSInfoMessage.h" #import "TSNetworkManager.h" #import "TSOutgoingMessage.h" #import "TSQuotedMessage.h" #import #import #import #import #import #import #import #import "OWSDispatch.h" #import "OWSBatchMessageProcessor.h" #import "OWSQueues.h" NS_ASSUME_NONNULL_BEGIN @interface OWSMessageManager () @property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; @property (nonatomic, readonly) YapDatabaseConnection *dbConnection; @property (nonatomic, readonly) OWSIncomingMessageFinder *incomingMessageFinder; @end #pragma mark - @implementation OWSMessageManager + (instancetype)sharedManager { OWSAssertDebug(SSKEnvironment.shared.messageManager); return SSKEnvironment.shared.messageManager; } - (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage { self = [super init]; if (!self) { return self; } _primaryStorage = primaryStorage; _dbConnection = primaryStorage.newDatabaseConnection; _incomingMessageFinder = [[OWSIncomingMessageFinder alloc] initWithPrimaryStorage:primaryStorage]; OWSSingletonAssert(); return self; } - (void)dealloc { [NSNotificationCenter.defaultCenter removeObserver:self]; } #pragma mark - Dependencies - (id)callMessageHandler { OWSAssertDebug(SSKEnvironment.shared.callMessageHandler); return SSKEnvironment.shared.callMessageHandler; } - (id)contactsManager { OWSAssertDebug(SSKEnvironment.shared.contactsManager); return SSKEnvironment.shared.contactsManager; } - (SSKMessageSenderJobQueue *)messageSenderJobQueue { return SSKEnvironment.shared.messageSenderJobQueue; } - (OWSBlockingManager *)blockingManager { OWSAssertDebug(SSKEnvironment.shared.blockingManager); return SSKEnvironment.shared.blockingManager; } - (OWSIdentityManager *)identityManager { OWSAssertDebug(SSKEnvironment.shared.identityManager); return SSKEnvironment.shared.identityManager; } - (TSNetworkManager *)networkManager { OWSAssertDebug(SSKEnvironment.shared.networkManager); return SSKEnvironment.shared.networkManager; } - (OWSOutgoingReceiptManager *)outgoingReceiptManager { OWSAssertDebug(SSKEnvironment.shared.outgoingReceiptManager); return SSKEnvironment.shared.outgoingReceiptManager; } - (id)syncManager { OWSAssertDebug(SSKEnvironment.shared.syncManager); return SSKEnvironment.shared.syncManager; } - (TSAccountManager *)tsAccountManager { OWSAssertDebug(SSKEnvironment.shared.tsAccountManager); return SSKEnvironment.shared.tsAccountManager; } - (id)profileManager { return SSKEnvironment.shared.profileManager; } - (id)typingIndicators { return SSKEnvironment.shared.typingIndicators; } - (OWSAttachmentDownloads *)attachmentDownloads { return SSKEnvironment.shared.attachmentDownloads; } #pragma mark - - (void)startObserving { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(yapDatabaseModified:) name:YapDatabaseModifiedNotification object:OWSPrimaryStorage.sharedManager.dbNotificationObject]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(yapDatabaseModified:) name:YapDatabaseModifiedExternallyNotification object:nil]; } - (void)yapDatabaseModified:(NSNotification *)notification { if (AppReadiness.isAppReady) { [OWSMessageUtils.sharedManager updateApplicationBadgeCount]; } else { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [AppReadiness runNowOrWhenAppDidBecomeReady:^{ [OWSMessageUtils.sharedManager updateApplicationBadgeCount]; }]; }); } } #pragma mark - Blocking - (BOOL)isEnvelopeSenderBlocked:(SSKProtoEnvelope *)envelope { OWSAssertDebug(envelope); return [self.blockingManager isRecipientIdBlocked:envelope.source]; } - (BOOL)isDataMessageBlocked:(SSKProtoDataMessage *)dataMessage envelope:(SSKProtoEnvelope *)envelope { OWSAssertDebug(dataMessage); OWSAssertDebug(envelope); if (dataMessage.group) { return [self.blockingManager isGroupIdBlocked:dataMessage.group.id]; } else { BOOL senderBlocked = [self isEnvelopeSenderBlocked:envelope]; // If the envelopeSender was blocked, we never should have gotten as far as decrypting the dataMessage. OWSAssertDebug(!senderBlocked); return senderBlocked; } } #pragma mark - message handling - (void)throws_processEnvelope:(SSKProtoEnvelope *)envelope plaintextData:(NSData *_Nullable)plaintextData wasReceivedByUD:(BOOL)wasReceivedByUD transaction:(YapDatabaseReadWriteTransaction *)transaction serverID:(uint64_t)serverID { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } if (!self.tsAccountManager.isRegistered) { OWSFailDebug(@"Not registered."); return; } OWSLogInfo(@"Handling decrypted envelope: %@.", [self descriptionForEnvelope:envelope]); if (!wasReceivedByUD) { if (!envelope.hasSource || envelope.source.length < 1) { OWSFailDebug(@"Incoming envelope with invalid source."); return; } if (!envelope.hasSourceDevice || envelope.sourceDevice < 1) { OWSFailDebug(@"Incoming envelope with invalid source device."); return; } } OWSAssertDebug(![self isEnvelopeSenderBlocked:envelope]); // Loki: Ignore any friend requests from before restoration // The envelope type is set during UD decryption. uint64_t restorationTime = [NSNumber numberWithDouble:[OWSPrimaryStorage.sharedManager getRestorationTime]].unsignedLongLongValue; if (envelope.type == SSKProtoEnvelopeTypeFriendRequest && envelope.timestamp < restorationTime * 1000) { [LKLogger print:@"[Loki] Ignoring friend request received before restoration."]; return; } [self checkForUnknownLinkedDevice:envelope transaction:transaction]; switch (envelope.type) { case SSKProtoEnvelopeTypeFriendRequest: case SSKProtoEnvelopeTypeCiphertext: case SSKProtoEnvelopeTypePrekeyBundle: case SSKProtoEnvelopeTypeUnidentifiedSender: if (!plaintextData) { OWSFailDebug(@"missing decrypted data for envelope: %@", [self descriptionForEnvelope:envelope]); return; } [self throws_handleEnvelope:envelope plaintextData:plaintextData wasReceivedByUD:wasReceivedByUD transaction:transaction serverID:serverID]; break; case SSKProtoEnvelopeTypeReceipt: OWSAssertDebug(!plaintextData); [self handleDeliveryReceipt:envelope transaction:transaction]; break; // Other messages are just dismissed for now. case SSKProtoEnvelopeTypeKeyExchange: OWSLogWarn(@"Received Key Exchange Message, not supported"); break; case SSKProtoEnvelopeTypeUnknown: OWSLogWarn(@"Received an unknown message type"); break; default: OWSLogWarn(@"Received unhandled envelope type: %d", (int)envelope.type); break; } } - (void)handleDeliveryReceipt:(SSKProtoEnvelope *)envelope transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } // Old-style delivery notices don't include a "delivery timestamp". [self processDeliveryReceiptsFromRecipientId:envelope.source sentTimestamps:@[ @(envelope.timestamp), ] deliveryTimestamp:nil transaction:transaction]; } // deliveryTimestamp is an optional parameter, since legacy // delivery receipts don't have a "delivery timestamp". Those // messages repurpose the "timestamp" field to indicate when the // corresponding message was originally sent. - (void)processDeliveryReceiptsFromRecipientId:(NSString *)recipientId sentTimestamps:(NSArray *)sentTimestamps deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp transaction:(YapDatabaseReadWriteTransaction *)transaction { if (recipientId.length < 1) { OWSFailDebug(@"Empty recipientId."); return; } if (sentTimestamps.count < 1) { OWSFailDebug(@"Missing sentTimestamps."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } for (NSNumber *nsTimestamp in sentTimestamps) { uint64_t timestamp = [nsTimestamp unsignedLongLongValue]; NSArray *messages = (NSArray *)[TSInteraction interactionsWithTimestamp:timestamp ofClass:[TSOutgoingMessage class] withTransaction:transaction]; if (messages.count < 1) { // The service sends delivery receipts for "unpersisted" messages // like group updates, so these errors are expected to a certain extent. // // TODO: persist "early" delivery receipts. OWSLogInfo(@"Missing message for delivery receipt: %llu", timestamp); } else { if (messages.count > 1) { OWSLogInfo(@"More than one message (%lu) for delivery receipt: %llu", (unsigned long)messages.count, timestamp); } for (TSOutgoingMessage *outgoingMessage in messages) { [outgoingMessage updateWithDeliveredRecipient:recipientId deliveryTimestamp:deliveryTimestamp transaction:transaction]; } } } } - (void)throws_handleEnvelope:(SSKProtoEnvelope *)envelope plaintextData:(NSData *)plaintextData wasReceivedByUD:(BOOL)wasReceivedByUD transaction:(YapDatabaseReadWriteTransaction *)transaction serverID:(uint64_t)serverID { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!plaintextData) { OWSFailDebug(@"Missing plaintextData."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } if (envelope.timestamp < 1) { OWSFailDebug(@"Invalid timestamp."); return; } if (envelope.source.length < 1) { OWSFailDebug(@"Missing source."); return; } if (envelope.sourceDevice < 1) { OWSFailDebug(@"Invalid source device."); return; } BOOL duplicateEnvelope = [self.incomingMessageFinder existsMessageWithTimestamp:envelope.timestamp sourceId:envelope.source sourceDeviceId:envelope.sourceDevice transaction:transaction]; if (duplicateEnvelope) { OWSLogInfo(@"Ignoring previously received envelope from: %@ with timestamp: %llu.", envelopeAddress(envelope), envelope.timestamp); return; } // Loki: Handle friend request acceptance if needed // The envelope type is set during UD decryption. [self handleFriendRequestAcceptanceIfNeededWithEnvelope:envelope transaction:transaction]; if (envelope.content != nil) { NSError *error; SSKProtoContent *_Nullable contentProto = [SSKProtoContent parseData:plaintextData error:&error]; if (error || !contentProto) { OWSFailDebug(@"Could not parse proto due to error: %@.", error); return; } OWSLogInfo(@"Handling content: .", [self descriptionForContent:contentProto]); // Loki: Workaround for duplicate sync transcript issue if (contentProto.syncMessage != nil && contentProto.syncMessage.sent != nil) { BOOL isDuplicate = [LKAPI isDuplicateSyncMessage:contentProto.syncMessage.sent from:envelope.source]; if (isDuplicate) { return; } } // Loki: Handle pre key bundle message if needed if (contentProto.prekeyBundleMessage != nil) { [LKLogger print:[NSString stringWithFormat:@"[Loki] Received a pre key bundle message from: %@.", envelope.source]]; PreKeyBundle *_Nullable bundle = [contentProto.prekeyBundleMessage getPreKeyBundleWithTransaction:transaction]; if (bundle == nil) { OWSFailDebug(@"Failed to create a pre key bundle."); return; } [self.primaryStorage setPreKeyBundle:bundle forContact:envelope.source transaction:transaction]; // Loki: If we received a friend request, but we were already friends with this user, reset the session // The envelope type is set during UD decryption. if (envelope.type == SSKProtoEnvelopeTypeFriendRequest) { TSContactThread *thread = [TSContactThread getThreadWithContactId:envelope.source transaction:transaction]; if (thread && thread.isContactFriend) { [self resetSessionWithContact:envelope.source transaction:transaction]; // Let our other devices know that we have reset the session [SSKEnvironment.shared.syncManager syncContact:envelope.source transaction:transaction]; } } } // Loki: Handle address message if needed if (contentProto.lokiAddressMessage) { NSString *address = contentProto.lokiAddressMessage.ptpAddress; uint32_t port = contentProto.lokiAddressMessage.ptpPort; [LKP2PAPI didReceiveLokiAddressMessageForContact:envelope.source address:address port:port receivedThroughP2P:envelope.isPtpMessage]; } // Loki: Handle device linking message if needed if (contentProto.lokiDeviceLinkMessage != nil) { NSString *masterHexEncodedPublicKey = contentProto.lokiDeviceLinkMessage.masterHexEncodedPublicKey; NSString *slaveHexEncodedPublicKey = contentProto.lokiDeviceLinkMessage.slaveHexEncodedPublicKey; NSData *masterSignature = contentProto.lokiDeviceLinkMessage.masterSignature; NSData *slaveSignature = contentProto.lokiDeviceLinkMessage.slaveSignature; if (masterSignature != nil) { // Authorization [LKLogger print:[NSString stringWithFormat:@"[Loki] Received a device linking authorization from: %@", envelope.source]]; // Not masterHexEncodedPublicKey [LKDeviceLinkingSession.current processLinkingAuthorizationFrom:masterHexEncodedPublicKey for:slaveHexEncodedPublicKey masterSignature:masterSignature slaveSignature:slaveSignature]; // Set any profile info if (contentProto.dataMessage) { SSKProtoDataMessage *dataMessage = contentProto.dataMessage; [self handleProfileNameUpdateIfNeeded:dataMessage recipientId:masterHexEncodedPublicKey transaction:transaction]; [self handleProfileKeyUpdateIfNeeded:dataMessage recipientId:masterHexEncodedPublicKey]; } } else if (slaveSignature != nil) { // Request [LKLogger print: [NSString stringWithFormat:@"[Loki] Received a device linking request from: %@", envelope.source]]; // Not slaveHexEncodedPublicKey if (LKDeviceLinkingSession.current == nil) { [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.unexpectedDeviceLinkRequestReceived object:nil]; } [LKDeviceLinkingSession.current processLinkingRequestFrom:slaveHexEncodedPublicKey to:masterHexEncodedPublicKey with:slaveSignature]; } } else if (contentProto.syncMessage) { [self throws_handleIncomingEnvelope:envelope withSyncMessage:contentProto.syncMessage transaction:transaction serverID:serverID]; [[OWSDeviceManager sharedManager] setHasReceivedSyncMessage]; } else if (contentProto.dataMessage) { [self handleIncomingEnvelope:envelope withDataMessage:contentProto.dataMessage wasReceivedByUD:wasReceivedByUD transaction:transaction]; } else if (contentProto.callMessage) { [self handleIncomingEnvelope:envelope withCallMessage:contentProto.callMessage]; } else if (contentProto.typingMessage) { [self handleIncomingEnvelope:envelope withTypingMessage:contentProto.typingMessage transaction:transaction]; } else if (contentProto.nullMessage) { OWSLogInfo(@"Received null message."); } else if (contentProto.receiptMessage) { [self handleIncomingEnvelope:envelope withReceiptMessage:contentProto.receiptMessage transaction:transaction]; } else { OWSLogWarn(@"Ignoring envelope. Content with no known payload"); } } else if (envelope.legacyMessage != nil) { // DEPRECATED - Remove after all clients have been upgraded. NSError *error; SSKProtoDataMessage *_Nullable dataMessageProto = [SSKProtoDataMessage parseData:plaintextData error:&error]; if (error || !dataMessageProto) { OWSFailDebug(@"could not parse proto: %@", error); return; } OWSLogInfo(@"handling message: ", [self descriptionForDataMessage:dataMessageProto]); [self handleIncomingEnvelope:envelope withDataMessage:dataMessageProto wasReceivedByUD:wasReceivedByUD transaction:transaction]; } else { OWSProdInfoWEnvelope([OWSAnalyticsEvents messageManagerErrorEnvelopeNoActionablePayload], envelope); } } - (void)handleIncomingEnvelope:(SSKProtoEnvelope *)envelope withDataMessage:(SSKProtoDataMessage *)dataMessage wasReceivedByUD:(BOOL)wasReceivedByUD transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!dataMessage) { OWSFailDebug(@"Missing dataMessage."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } // Loki: Don't process session request messages any further if ((dataMessage.flags & SSKProtoDataMessageFlagsSessionRequest) != 0) { return; } // Loki: Don't process session restore messages any further if ((dataMessage.flags & SSKProtoDataMessageFlagsSessionRestore) != 0) { return; } if ([self isDataMessageBlocked:dataMessage envelope:envelope]) { NSString *logMessage = [NSString stringWithFormat:@"Ignoring blocked message from sender: %@.", envelope.source]; if (dataMessage.group) { logMessage = [logMessage stringByAppendingFormat:@" in group: %@", dataMessage.group.id]; } OWSLogError(@"%@", logMessage); return; } if (dataMessage.hasTimestamp) { if (dataMessage.timestamp <= 0) { OWSFailDebug(@"Ignoring data message with invalid timestamp: %@.", envelope.source); return; } // This prevents replay attacks by the service. if (dataMessage.timestamp != envelope.timestamp) { OWSFailDebug(@"Ignoring data message with non-matching timestamp: %@.", envelope.source); return; } } [self handleProfileKeyUpdateIfNeeded:dataMessage recipientId:envelope.source]; if (dataMessage.group) { TSGroupThread *_Nullable groupThread = [TSGroupThread threadWithGroupId:dataMessage.group.id transaction:transaction]; if (groupThread) { if (dataMessage.group.type != SSKProtoGroupContextTypeUpdate) { if (![groupThread isLocalUserInGroupWithTransaction:transaction]) { OWSLogInfo(@"Ignoring messages for left group."); return; } } } else { // Unknown group. if (dataMessage.group.type == SSKProtoGroupContextTypeUpdate) { // Accept group updates for unknown groups. } else if (dataMessage.group.type == SSKProtoGroupContextTypeDeliver) { [self sendGroupInfoRequest:dataMessage.group.id envelope:envelope transaction:transaction]; return; } else { OWSLogInfo(@"Ignoring group message for unknown group from: %@.", envelope.source); return; } } } if ((dataMessage.flags & SSKProtoDataMessageFlagsEndSession) != 0) { [self handleEndSessionMessageWithEnvelope:envelope dataMessage:dataMessage transaction:transaction]; } else if ((dataMessage.flags & SSKProtoDataMessageFlagsExpirationTimerUpdate) != 0) { [self handleExpirationTimerUpdateMessageWithEnvelope:envelope dataMessage:dataMessage transaction:transaction]; } else if ((dataMessage.flags & SSKProtoDataMessageFlagsProfileKeyUpdate) != 0) { [self handleProfileKeyMessageWithEnvelope:envelope dataMessage:dataMessage]; } else if ((dataMessage.flags & SSKProtoDataMessageFlagsUnlinkDevice) != 0) { [self handleUnlinkDeviceMessageWithEnvelope:envelope dataMessage:dataMessage transaction:transaction]; } else if (dataMessage.attachments.count > 0) { [self handleReceivedMediaWithEnvelope:envelope dataMessage:dataMessage wasReceivedByUD:wasReceivedByUD transaction:transaction]; } else { [self handleReceivedTextMessageWithEnvelope:envelope dataMessage:dataMessage wasReceivedByUD:wasReceivedByUD transaction:transaction]; if ([self isDataMessageGroupAvatarUpdate:dataMessage]) { OWSLogVerbose(@"Data message had group avatar attachment"); [self handleReceivedGroupAvatarUpdateWithEnvelope:envelope dataMessage:dataMessage transaction:transaction]; } } // Send delivery receipts for "valid data" messages received via UD. if (wasReceivedByUD) { [self.outgoingReceiptManager enqueueDeliveryReceiptForEnvelope:envelope]; } } - (void)sendGroupInfoRequest:(NSData *)groupId envelope:(SSKProtoEnvelope *)envelope transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } if (groupId.length < 1) { OWSFailDebug(@"Invalid groupId."); return; } // FIXME: https://github.com/signalapp/Signal-iOS/issues/1340 OWSLogInfo(@"Sending group info request: %@", envelopeAddress(envelope)); NSString *recipientId = envelope.source; TSThread *thread = [TSContactThread getOrCreateThreadWithContactId:recipientId transaction:transaction]; OWSSyncGroupsRequestMessage *syncGroupsRequestMessage = [[OWSSyncGroupsRequestMessage alloc] initWithThread:thread groupId:groupId]; [self.messageSenderJobQueue addMessage:syncGroupsRequestMessage transaction:transaction]; } - (void)handleIncomingEnvelope:(SSKProtoEnvelope *)envelope withReceiptMessage:(SSKProtoReceiptMessage *)receiptMessage transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!receiptMessage) { OWSFailDebug(@"Missing receiptMessage."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } NSArray *sentTimestamps = receiptMessage.timestamp; switch (receiptMessage.type) { case SSKProtoReceiptMessageTypeDelivery: OWSLogVerbose(@"Processing receipt message with delivery receipts."); [self processDeliveryReceiptsFromRecipientId:envelope.source sentTimestamps:sentTimestamps deliveryTimestamp:@(envelope.timestamp) transaction:transaction]; return; case SSKProtoReceiptMessageTypeRead: OWSLogVerbose(@"Processing receipt message with read receipts."); [OWSReadReceiptManager.sharedManager processReadReceiptsFromRecipientId:envelope.source sentTimestamps:sentTimestamps readTimestamp:envelope.timestamp]; break; default: OWSLogInfo(@"Ignoring receipt message of unknown type: %d.", (int)receiptMessage.type); return; } } - (void)handleIncomingEnvelope:(SSKProtoEnvelope *)envelope withCallMessage:(SSKProtoCallMessage *)callMessage { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!callMessage) { OWSFailDebug(@"Missing callMessage."); return; } if ([callMessage hasProfileKey]) { NSData *profileKey = [callMessage profileKey]; NSString *recipientId = envelope.source; [self.profileManager setProfileKeyData:profileKey forRecipientId:recipientId]; } // By dispatching async, we introduce the possibility that these messages might be lost // if the app exits before this block is executed. This is fine, since the call by // definition will end if the app exits. dispatch_async(dispatch_get_main_queue(), ^{ if (callMessage.offer) { [self.callMessageHandler receivedOffer:callMessage.offer fromCallerId:envelope.source]; } else if (callMessage.answer) { [self.callMessageHandler receivedAnswer:callMessage.answer fromCallerId:envelope.source]; } else if (callMessage.iceUpdate.count > 0) { for (SSKProtoCallMessageIceUpdate *iceUpdate in callMessage.iceUpdate) { [self.callMessageHandler receivedIceUpdate:iceUpdate fromCallerId:envelope.source]; } } else if (callMessage.hangup) { OWSLogVerbose(@"Received CallMessage with Hangup."); [self.callMessageHandler receivedHangup:callMessage.hangup fromCallerId:envelope.source]; } else if (callMessage.busy) { [self.callMessageHandler receivedBusy:callMessage.busy fromCallerId:envelope.source]; } else { OWSProdInfoWEnvelope([OWSAnalyticsEvents messageManagerErrorCallMessageNoActionablePayload], envelope); } }); } - (void)handleIncomingEnvelope:(SSKProtoEnvelope *)envelope withTypingMessage:(SSKProtoTypingMessage *)typingMessage transaction:(YapDatabaseReadWriteTransaction *)transaction { OWSAssertDebug(transaction); if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!typingMessage) { OWSFailDebug(@"Missing typingMessage."); return; } if (typingMessage.timestamp != envelope.timestamp) { OWSFailDebug(@"typingMessage has invalid timestamp."); return; } NSString *localNumber = self.tsAccountManager.localNumber; if ([localNumber isEqualToString:envelope.source]) { OWSLogVerbose(@"Ignoring typing indicators from self or linked device."); return; } else if ([self.blockingManager isRecipientIdBlocked:envelope.source] || (typingMessage.hasGroupID && [self.blockingManager isGroupIdBlocked:typingMessage.groupID])) { NSString *logMessage = [NSString stringWithFormat:@"Ignoring blocked message from sender: %@", envelope.source]; if (typingMessage.hasGroupID) { logMessage = [logMessage stringByAppendingFormat:@" in group: %@", typingMessage.groupID]; } OWSLogError(@"%@", logMessage); return; } TSThread *_Nullable thread; if (typingMessage.hasGroupID) { TSGroupThread *groupThread = [TSGroupThread threadWithGroupId:typingMessage.groupID transaction:transaction]; if (![groupThread isLocalUserInGroupWithTransaction:transaction]) { OWSLogInfo(@"Ignoring messages for left group."); return; } thread = groupThread; } else { thread = [TSContactThread getThreadWithContactId:envelope.source transaction:transaction]; } if (!thread) { // This isn't neccesarily an error. We might not yet know about the thread, // in which case we don't need to display the typing indicators. OWSLogWarn(@"Could not locate thread for typingMessage."); return; } dispatch_async(dispatch_get_main_queue(), ^{ switch (typingMessage.action) { case SSKProtoTypingMessageActionStarted: [self.typingIndicators didReceiveTypingStartedMessageInThread:thread recipientId:envelope.source deviceId:envelope.sourceDevice]; break; case SSKProtoTypingMessageActionStopped: [self.typingIndicators didReceiveTypingStoppedMessageInThread:thread recipientId:envelope.source deviceId:envelope.sourceDevice]; break; default: OWSFailDebug(@"Typing message has unexpected action."); break; } }); } - (void)handleReceivedGroupAvatarUpdateWithEnvelope:(SSKProtoEnvelope *)envelope dataMessage:(SSKProtoDataMessage *)dataMessage transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!dataMessage) { OWSFailDebug(@"Missing dataMessage."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } TSGroupThread *_Nullable groupThread = [TSGroupThread threadWithGroupId:dataMessage.group.id transaction:transaction]; if (!groupThread) { OWSFailDebug(@"Missing group for group avatar update"); return; } TSAttachmentPointer *_Nullable avatarPointer = [TSAttachmentPointer attachmentPointerFromProto:dataMessage.group.avatar albumMessage:nil]; if (!avatarPointer) { OWSLogWarn(@"received unsupported group avatar envelope"); return; } [self.attachmentDownloads downloadAttachmentPointer:avatarPointer success:^(NSArray *attachmentStreams) { OWSAssertDebug(attachmentStreams.count == 1); TSAttachmentStream *attachmentStream = attachmentStreams.firstObject; [groupThread updateAvatarWithAttachmentStream:attachmentStream]; } failure:^(NSError *error) { OWSLogError(@"failed to fetch attachments for group avatar sent at: %llu. with error: %@", envelope.timestamp, error); }]; } - (void)handleReceivedMediaWithEnvelope:(SSKProtoEnvelope *)envelope dataMessage:(SSKProtoDataMessage *)dataMessage wasReceivedByUD:(BOOL)wasReceivedByUD transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!dataMessage) { OWSFailDebug(@"Missing dataMessage."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } TSThread *_Nullable thread = [self threadForEnvelope:envelope dataMessage:dataMessage transaction:transaction]; if (!thread) { OWSFailDebug(@"Ignoring media message for unknown group."); return; } TSIncomingMessage *_Nullable message = [self handleReceivedEnvelope:envelope withDataMessage:dataMessage wasReceivedByUD:wasReceivedByUD transaction:transaction]; if (!message) { return; } [message saveWithTransaction:transaction]; OWSLogDebug(@"Incoming attachment message: %@.", message.debugDescription); [self.attachmentDownloads downloadAttachmentsForMessage:message transaction:transaction success:^(NSArray *attachmentStreams) { OWSLogDebug(@"Successfully fetched attachments: %lu for message: %@.", (unsigned long)attachmentStreams.count, message); } failure:^(NSError *error) { OWSLogError(@"Failed to fetch attachments for message: %@ with error: %@.", message, error); }]; } - (void)throws_handleIncomingEnvelope:(SSKProtoEnvelope *)envelope withSyncMessage:(SSKProtoSyncMessage *)syncMessage transaction:(YapDatabaseReadWriteTransaction *)transaction serverID:(uint64_t)serverID { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!syncMessage) { OWSFailDebug(@"Missing syncMessage."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; NSSet *linkedDeviceHexEncodedPublicKeys = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userHexEncodedPublicKey in:transaction]; if (![linkedDeviceHexEncodedPublicKeys contains:^BOOL(NSString *hexEncodedPublicKey) { return [hexEncodedPublicKey isEqual:envelope.source]; }]) { OWSProdErrorWEnvelope([OWSAnalyticsEvents messageManagerErrorSyncMessageFromUnknownSource], envelope); return; } NSString *masterHexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userHexEncodedPublicKey in:transaction]; BOOL wasSentByMasterDevice = [masterHexEncodedPublicKey isEqual:envelope.source]; if (syncMessage.sent) { OWSIncomingSentMessageTranscript *transcript = [[OWSIncomingSentMessageTranscript alloc] initWithProto:syncMessage.sent transaction:transaction]; SSKProtoDataMessage *_Nullable dataMessage = syncMessage.sent.message; if (!dataMessage) { OWSFailDebug(@"Missing dataMessage."); return; } // Loki: Try to update using the provided profile if (wasSentByMasterDevice) { [self handleProfileNameUpdateIfNeeded:dataMessage recipientId:masterHexEncodedPublicKey transaction:transaction]; [self handleProfileKeyUpdateIfNeeded:dataMessage recipientId:masterHexEncodedPublicKey]; } NSString *destination = syncMessage.sent.destination; if (dataMessage && destination.length > 0 && dataMessage.hasProfileKey) { // If we observe a linked device sending our profile key to another // user, we can infer that that user belongs in our profile whitelist. if (dataMessage.group) { [self.profileManager addGroupIdToProfileWhitelist:dataMessage.group.id]; } else { [self.profileManager addUserToProfileWhitelist:destination]; } } if ([self isDataMessageGroupAvatarUpdate:syncMessage.sent.message] && !syncMessage.sent.isRecipientUpdate) { [OWSRecordTranscriptJob processIncomingSentMessageTranscript:transcript serverID:0 attachmentHandler:^(NSArray *attachmentStreams) { OWSAssertDebug(attachmentStreams.count == 1); TSAttachmentStream *attachmentStream = attachmentStreams.firstObject; [self.dbConnection readWriteWithBlock:^( YapDatabaseReadWriteTransaction *transaction) { TSGroupThread *_Nullable groupThread = [TSGroupThread threadWithGroupId:dataMessage.group.id transaction:transaction]; if (!groupThread) { OWSFailDebug(@"ignoring sync group avatar update for unknown group."); return; } [groupThread updateAvatarWithAttachmentStream:attachmentStream transaction:transaction]; }]; } transaction:transaction]; } else { if (transcript.isGroupUpdate) { // TODO: This code is pretty much a duplicate of the code in OWSRecordTranscriptJob TSGroupThread *newGroupThread = [TSGroupThread getOrCreateThreadWithGroupId:transcript.dataMessage.group.id groupType:closedGroup transaction:transaction]; TSGroupModel *newGroupModel = [[TSGroupModel alloc] initWithTitle:transcript.dataMessage.group.name memberIds:transcript.dataMessage.group.members image:nil groupId:transcript.dataMessage.group.id groupType:closedGroup adminIds:transcript.dataMessage.group.admins]; NSString *updateMessage = [newGroupThread.groupModel getInfoStringAboutUpdateTo:newGroupModel contactsManager:self.contactsManager]; newGroupThread.groupModel = newGroupModel; [newGroupThread saveWithTransaction:transaction]; // Loki: Try to establish sessions with all members when a group is created or updated [self establishSessionsWithMembersIfNeeded: transcript.dataMessage.group.members forThread:newGroupThread transaction:transaction]; [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:transcript.dataMessage.expireTimer thread:newGroupThread createdByRemoteRecipientId:nil createdInExistingGroup:YES transaction:transaction]; TSInfoMessage *infoMessage = [[TSInfoMessage alloc] initWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:newGroupThread messageType:TSInfoMessageTypeGroupUpdate customMessage:updateMessage]; [infoMessage saveWithTransaction:transaction]; } else if (transcript.isGroupQuit) { TSGroupThread *groupThread = [TSGroupThread getOrCreateThreadWithGroupId:transcript.dataMessage.group.id groupType:closedGroup transaction:transaction]; [groupThread leaveGroupWithTransaction:transaction]; TSInfoMessage *infoMessage = [[TSInfoMessage alloc] initWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:groupThread messageType:TSInfoMessageTypeGroupQuit customMessage:NSLocalizedString(@"GROUP_YOU_LEFT", nil)]; [infoMessage saveWithTransaction:transaction]; } else { [OWSRecordTranscriptJob processIncomingSentMessageTranscript:transcript serverID:(serverID ?: 0) attachmentHandler:^(NSArray *attachmentStreams) { OWSLogDebug(@"successfully fetched transcript attachments: %lu", (unsigned long)attachmentStreams.count); } transaction:transaction]; } } } else if (syncMessage.request) { if (syncMessage.request.type == SSKProtoSyncMessageRequestTypeContacts) { // We respond asynchronously because populating the sync message will // create transactions and it's not practical (due to locking in the OWSIdentityManager) // to plumb our transaction through. // // In rare cases this means we won't respond to the sync request, but that's // acceptable. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[self.syncManager syncAllContacts] retainUntilComplete]; }); } else if (syncMessage.request.type == SSKProtoSyncMessageRequestTypeGroups) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[self.syncManager syncAllGroups] retainUntilComplete]; }); } else if (syncMessage.request.type == SSKProtoSyncMessageRequestTypeBlocked) { OWSLogInfo(@"Received request for block list"); [self.blockingManager syncBlockList]; } else if (syncMessage.request.type == SSKProtoSyncMessageRequestTypeConfiguration) { [SSKEnvironment.shared.syncManager sendConfigurationSyncMessage]; } else { OWSLogWarn(@"ignoring unsupported sync request message"); } } else if (syncMessage.blocked) { NSArray *blockedPhoneNumbers = [syncMessage.blocked.numbers copy]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self.blockingManager setBlockedPhoneNumbers:blockedPhoneNumbers sendSyncMessage:NO]; }); } else if (syncMessage.read.count > 0) { OWSLogInfo(@"Received %lu read receipt(s)", (unsigned long)syncMessage.read.count); [OWSReadReceiptManager.sharedManager processReadReceiptsFromLinkedDevice:syncMessage.read readTimestamp:envelope.timestamp transaction:transaction]; } else if (syncMessage.verified) { OWSLogInfo(@"Received verification state for %@", syncMessage.verified.destination); [self.identityManager throws_processIncomingSyncMessage:syncMessage.verified transaction:transaction]; } else if (syncMessage.contacts != nil) { if (wasSentByMasterDevice && syncMessage.contacts.data.length > 0) { [LKLogger print:@"[Loki] Received contact sync message."]; NSData *data = syncMessage.contacts.data; ContactParser *parser = [[ContactParser alloc] initWithData:data]; NSArray *hexEncodedPublicKeys = [parser parseHexEncodedPublicKeys]; // Try to establish sessions for (NSString *hexEncodedPublicKey in hexEncodedPublicKeys) { TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction]; LKThreadFriendRequestStatus friendRequestStatus = thread.friendRequestStatus; switch (friendRequestStatus) { case LKThreadFriendRequestStatusNone: { OWSMessageSender *messageSender = SSKEnvironment.shared.messageSender; LKFriendRequestMessage *automatedFriendRequestMessage = [messageSender getMultiDeviceFriendRequestMessageForHexEncodedPublicKey:hexEncodedPublicKey transaction:transaction]; thread.isForceHidden = true; [thread saveWithTransaction:transaction]; [messageSender sendMessage:automatedFriendRequestMessage success:^{ [automatedFriendRequestMessage remove]; thread.isForceHidden = false; [thread save]; } failure:^(NSError *error) { [automatedFriendRequestMessage remove]; thread.isForceHidden = false; [thread save]; }]; break; } case LKThreadFriendRequestStatusRequestReceived: { [thread saveFriendRequestStatus:LKThreadFriendRequestStatusFriends withTransaction:transaction]; // The two lines below are equivalent to calling [ThreadUtil enqueueFriendRequestAcceptanceMessageInThread:thread] LKEphemeralMessage *backgroundMessage = [[LKEphemeralMessage alloc] initInThread:thread]; [self.messageSenderJobQueue addMessage:backgroundMessage transaction:transaction]; break; } default: break; // Do nothing } } } } else if (syncMessage.groups != nil) { if (wasSentByMasterDevice && syncMessage.groups.data.length > 0) { [LKLogger print:@"[Loki] Received group sync message."]; NSData *data = syncMessage.groups.data; GroupParser *parser = [[GroupParser alloc] initWithData:data]; NSArray *groupModels = [parser parseGroupModels]; for (TSGroupModel *groupModel in groupModels) { TSGroupThread *thread = [TSGroupThread threadWithGroupId:groupModel.groupId transaction:transaction]; if (thread == nil) { thread = [TSGroupThread getOrCreateThreadWithGroupModel:groupModel transaction:transaction]; [thread saveWithTransaction:transaction]; [self establishSessionsWithMembersIfNeeded:groupModel.groupMemberIds forThread:thread transaction:transaction]; TSInfoMessage *infoMessage = [[TSInfoMessage alloc] initWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageType:TSInfoMessageTypeGroupUpdate customMessage:@"You have joined the group."]; [infoMessage saveWithTransaction:transaction]; } } } } else if (syncMessage.openGroups != nil) { if (wasSentByMasterDevice && syncMessage.openGroups.count > 0) { [LKLogger print:@"[Loki] Received open group sync message."]; for (SSKProtoSyncMessageOpenGroups* openGroup in syncMessage.openGroups) { [LKPublicChatManager.shared addChatWithServer:openGroup.url channel:openGroup.channel]; } } } else { OWSLogWarn(@"Ignoring unsupported sync message."); } } - (void)handleEndSessionMessageWithEnvelope:(SSKProtoEnvelope *)envelope dataMessage:(SSKProtoDataMessage *)dataMessage transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!dataMessage) { OWSFailDebug(@"Missing dataMessage."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } [self resetSessionWithContact:envelope.source transaction:transaction]; } - (void)resetSessionWithContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *)transaction { TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction]; // MJK TODO - safe to remove senderTimestamp [[[TSInfoMessage alloc] initWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageType:TSInfoMessageTypeLokiSessionResetInProgress] saveWithTransaction:transaction]; // Loki: Archive all our sessions // Ref: SignalServiceKit/Loki/Docs/SessionReset.md [self.primaryStorage archiveAllSessionsForContact:hexEncodedPublicKey protocolContext:transaction]; // Loki: Set our session reset state thread.sessionResetStatus = LKSessionResetStatusRequestReceived; [thread saveWithTransaction:transaction]; // Loki: Send an empty message to trigger the session reset code for both parties LKEphemeralMessage *emptyMessage = [[LKEphemeralMessage alloc] initInThread:thread]; [self.messageSenderJobQueue addMessage:emptyMessage transaction:transaction]; [LKLogger print:[NSString stringWithFormat:@"[Loki] Session reset received from %@.", hexEncodedPublicKey]]; } - (void)handleExpirationTimerUpdateMessageWithEnvelope:(SSKProtoEnvelope *)envelope dataMessage:(SSKProtoDataMessage *)dataMessage transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!dataMessage) { OWSFailDebug(@"Missing dataMessage."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } TSThread *_Nullable thread = [self threadForEnvelope:envelope dataMessage:dataMessage transaction:transaction]; if (!thread) { OWSFailDebug(@"ignoring expiring messages update for unknown group."); return; } OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration; if (dataMessage.hasExpireTimer && dataMessage.expireTimer > 0) { OWSLogInfo( @"Expiring messages duration turned to %u for thread %@", (unsigned int)dataMessage.expireTimer, thread); disappearingMessagesConfiguration = [[OWSDisappearingMessagesConfiguration alloc] initWithThreadId:thread.uniqueId enabled:YES durationSeconds:dataMessage.expireTimer]; } else { OWSLogInfo(@"Expiring messages have been turned off for thread %@", thread); disappearingMessagesConfiguration = [[OWSDisappearingMessagesConfiguration alloc] initWithThreadId:thread.uniqueId enabled:NO durationSeconds:OWSDisappearingMessagesConfigurationDefaultExpirationDuration]; } OWSAssertDebug(disappearingMessagesConfiguration); [disappearingMessagesConfiguration saveWithTransaction:transaction]; NSString *name = [dataMessage.profile displayName] ?: [self.contactsManager displayNameForPhoneIdentifier:envelope.source transaction:transaction]; // MJK TODO - safe to remove senderTimestamp OWSDisappearingConfigurationUpdateInfoMessage *message = [[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] thread:thread configuration:disappearingMessagesConfiguration createdByRemoteName:name createdInExistingGroup:NO]; [message saveWithTransaction:transaction]; } - (void)handleProfileKeyMessageWithEnvelope:(SSKProtoEnvelope *)envelope dataMessage:(SSKProtoDataMessage *)dataMessage { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!dataMessage) { OWSFailDebug(@"Missing dataMessage."); return; } NSString *recipientId = envelope.source; if (!dataMessage.hasProfileKey) { OWSFailDebug(@"received profile key message without profile key from: %@", envelopeAddress(envelope)); return; } NSData *profileKey = dataMessage.profileKey; if (profileKey.length != kAES256_KeyByteLength) { OWSFailDebug(@"received profile key of unexpected length: %lu, from: %@", (unsigned long)profileKey.length, envelopeAddress(envelope)); return; } if (dataMessage.profile == nil) { OWSFailDebug(@"received profile key message without loki profile attached from: %@", envelopeAddress(envelope)); return; } id profileManager = SSKEnvironment.shared.profileManager; [profileManager setProfileKeyData:profileKey forRecipientId:recipientId avatarURL:dataMessage.profile.profilePicture]; } - (void)handleUnlinkDeviceMessageWithEnvelope:(SSKProtoEnvelope *)envelope dataMessage:(SSKProtoDataMessage *)dataMessage transaction:(YapDatabaseReadWriteTransaction *)transaction { NSString *senderHexEncodedPublicKey = envelope.source; NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; NSString *masterHexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userHexEncodedPublicKey in:transaction]; if (![masterHexEncodedPublicKey isEqual:senderHexEncodedPublicKey]) { return; } NSSet *deviceLinks = [LKDatabaseUtilities getDeviceLinksFor:senderHexEncodedPublicKey in:transaction]; if (![deviceLinks contains:^BOOL(LKDeviceLink *deviceLink) { return [deviceLink.master.hexEncodedPublicKey isEqual:senderHexEncodedPublicKey] && [deviceLink.slave.hexEncodedPublicKey isEqual:userHexEncodedPublicKey]; }]) { return; } [LKFileServerAPI getDeviceLinksAssociatedWith:userHexEncodedPublicKey].thenOn(dispatch_get_main_queue(), ^(NSSet *deviceLinks) { if ([deviceLinks contains:^BOOL(LKDeviceLink *deviceLink) { return [deviceLink.master.hexEncodedPublicKey isEqual:senderHexEncodedPublicKey] && [deviceLink.slave.hexEncodedPublicKey isEqual:userHexEncodedPublicKey]; }]) { [NSUserDefaults.standardUserDefaults setBool:YES forKey:@"wasUnlinked"]; [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.dataNukeRequested object:nil]; } }); } - (void)handleReceivedTextMessageWithEnvelope:(SSKProtoEnvelope *)envelope dataMessage:(SSKProtoDataMessage *)dataMessage wasReceivedByUD:(BOOL)wasReceivedByUD transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!dataMessage) { OWSFailDebug(@"Missing dataMessage."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } [self handleReceivedEnvelope:envelope withDataMessage:dataMessage wasReceivedByUD:wasReceivedByUD transaction:transaction]; } - (void)handleGroupInfoRequest:(SSKProtoEnvelope *)envelope dataMessage:(SSKProtoDataMessage *)dataMessage transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!dataMessage) { OWSFailDebug(@"Missing dataMessage."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } if (dataMessage.group.type != SSKProtoGroupContextTypeRequestInfo) { OWSFailDebug(@"Unexpected group message type."); return; } NSData *groupId = dataMessage.group ? dataMessage.group.id : nil; if (!groupId) { OWSFailDebug(@"Group info request is missing group id."); return; } OWSLogInfo(@"Received 'Request Group Info' message for group: %@ from: %@", groupId, envelope.source); TSGroupThread *_Nullable gThread = [TSGroupThread threadWithGroupId:dataMessage.group.id transaction:transaction]; if (!gThread) { OWSLogWarn(@"Unknown group: %@", groupId); return; } // Ensure sender is in the group. if (![gThread isUserInGroup:envelope.source transaction:transaction]) { OWSLogWarn(@"Ignoring 'Request Group Info' message for non-member of group. %@ not in %@", envelope.source, gThread.groupModel.groupMemberIds); return; } // Ensure we are in the group. if (![gThread isLocalUserInGroupWithTransaction:transaction]) { OWSLogWarn(@"Ignoring 'Request Group Info' message for group we no longer belong to."); return; } NSString *updateGroupInfo = [gThread.groupModel getInfoStringAboutUpdateTo:gThread.groupModel contactsManager:self.contactsManager]; uint32_t expiresInSeconds = [gThread disappearingMessagesDurationWithTransaction:transaction]; TSOutgoingMessage *message = [TSOutgoingMessage outgoingMessageInThread:gThread groupMetaMessage:TSGroupMetaMessageUpdate expiresInSeconds:expiresInSeconds]; [message updateWithCustomMessage:updateGroupInfo transaction:transaction]; // Only send this group update to the requester. [message updateWithSendingToSingleGroupRecipient:envelope.source transaction:transaction]; if (gThread.groupModel.groupImage) { NSData *_Nullable data = UIImagePNGRepresentation(gThread.groupModel.groupImage); OWSAssertDebug(data); if (data) { DataSource *_Nullable dataSource = [DataSourceValue dataSourceWithData:data fileExtension:@"png"]; [self.messageSenderJobQueue addMediaMessage:message dataSource:dataSource contentType:OWSMimeTypeImagePng sourceFilename:nil caption:nil albumMessageId:nil isTemporaryAttachment:YES]; } } else { [self.messageSenderJobQueue addMessage:message transaction:transaction]; } } - (TSIncomingMessage *_Nullable)handleReceivedEnvelope:(SSKProtoEnvelope *)envelope withDataMessage:(SSKProtoDataMessage *)dataMessage wasReceivedByUD:(BOOL)wasReceivedByUD transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return nil; } if (!dataMessage) { OWSFailDebug(@"Missing dataMessage."); return nil; } if (!transaction) { OWSFail(@"Missing transaction."); return nil; } uint64_t timestamp = envelope.timestamp; NSString *body = dataMessage.body; NSData *groupId = dataMessage.group ? dataMessage.group.id : nil; OWSContact *_Nullable contact = [OWSContacts contactForDataMessage:dataMessage transaction:transaction]; NSNumber *_Nullable serverTimestamp = (envelope.hasServerTimestamp ? @(envelope.serverTimestamp) : nil); if (dataMessage.group.type == SSKProtoGroupContextTypeRequestInfo) { [self handleGroupInfoRequest:envelope dataMessage:dataMessage transaction:transaction]; return nil; } // The envelope source is set during UD decryption. if ([ECKeyPair isValidHexEncodedPublicKeyWithCandidate:envelope.source] && dataMessage.publicChatInfo == nil) { // Handled in LokiPublicChatPoller for open group messages dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [[LKAPI getDestinationsFor:envelope.source inTransaction:transaction].ensureOn(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^() { dispatch_semaphore_signal(semaphore); }).catchOn(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(NSError *error) { dispatch_semaphore_signal(semaphore); }) retainUntilComplete]; dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)); } if (groupId.length > 0) { NSMutableSet *newMemberIds = [NSMutableSet setWithArray:dataMessage.group.members]; NSMutableSet *removedMemberIds = [NSMutableSet new]; for (NSString *recipientId in newMemberIds) { if (![ECKeyPair isValidHexEncodedPublicKeyWithCandidate:recipientId]) { OWSLogVerbose( @"incoming group update has invalid group member: %@", [self descriptionForEnvelope:envelope]); OWSFailDebug(@"incoming group update has invalid group member"); return nil; } } NSString *hexEncodedPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source); // Group messages create the group if it doesn't already exist. // // We distinguish between the old group state (if any) and the new group state. TSGroupThread *_Nullable oldGroupThread = [TSGroupThread threadWithGroupId:groupId transaction:transaction]; if (oldGroupThread) { // Loki: Determine removed members removedMemberIds = [NSMutableSet setWithArray:oldGroupThread.groupModel.groupMemberIds]; [removedMemberIds minusSet:newMemberIds]; [removedMemberIds removeObject:hexEncodedPublicKey]; } // Only set the display name here, the logic for updating profile pictures is handled when we're setting profile key [self handleProfileNameUpdateIfNeeded:dataMessage recipientId:hexEncodedPublicKey transaction:transaction]; switch (dataMessage.group.type) { case SSKProtoGroupContextTypeUpdate: { if (oldGroupThread && ![oldGroupThread isUserAdminInGroup:hexEncodedPublicKey transaction:transaction]) { [LKLogger print:[NSString stringWithFormat:@"[Loki] Received a group update from a non-admin user for %@; ignoring.", [LKGroupUtilities getEncodedGroupID:groupId]]]; return nil; } // Ensures that the thread exists but doesn't update it. TSGroupThread *newGroupThread = [TSGroupThread getOrCreateThreadWithGroupId:groupId groupType:oldGroupThread.groupModel.groupType transaction:transaction]; TSGroupModel *newGroupModel = [[TSGroupModel alloc] initWithTitle:dataMessage.group.name memberIds:newMemberIds.allObjects image:oldGroupThread.groupModel.groupImage groupId:dataMessage.group.id groupType:oldGroupThread.groupModel.groupType adminIds:dataMessage.group.admins]; newGroupModel.removedMembers = removedMemberIds; NSString *updateGroupInfo = [newGroupThread.groupModel getInfoStringAboutUpdateTo:newGroupModel contactsManager:self.contactsManager]; newGroupThread.groupModel = newGroupModel; [newGroupThread saveWithTransaction:transaction]; // Loki: Try to establish sessions with all members when a group is created or updated [self establishSessionsWithMembersIfNeeded: newMemberIds.allObjects forThread:newGroupThread transaction:transaction]; [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer thread:newGroupThread createdByRemoteRecipientId:nil createdInExistingGroup:YES transaction:transaction]; // MJK TODO - should be safe to remove senderTimestamp TSInfoMessage *infoMessage = [[TSInfoMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:newGroupThread messageType:TSInfoMessageTypeGroupUpdate customMessage:updateGroupInfo]; [infoMessage saveWithTransaction:transaction]; return nil; } case SSKProtoGroupContextTypeQuit: { if (!oldGroupThread) { OWSLogWarn(@"ignoring quit group message from unknown group."); return nil; } newMemberIds = [NSMutableSet setWithArray:oldGroupThread.groupModel.groupMemberIds]; [newMemberIds removeObject:hexEncodedPublicKey]; oldGroupThread.groupModel.groupMemberIds = [newMemberIds.allObjects mutableCopy]; [oldGroupThread saveWithTransaction:transaction]; NSString *nameString = [self.contactsManager displayNameForPhoneIdentifier:hexEncodedPublicKey transaction:transaction]; NSString *updateGroupInfo = [NSString stringWithFormat:NSLocalizedString(@"GROUP_MEMBER_LEFT", @""), nameString]; // MJK TODO - should be safe to remove senderTimestamp [[[TSInfoMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:oldGroupThread messageType:TSInfoMessageTypeGroupUpdate customMessage:updateGroupInfo] saveWithTransaction:transaction]; return nil; } case SSKProtoGroupContextTypeDeliver: { if (!oldGroupThread) { OWSFailDebug(@"ignoring deliver group message from unknown group."); return nil; } [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer thread:oldGroupThread createdByRemoteRecipientId:hexEncodedPublicKey createdInExistingGroup:NO transaction:transaction]; TSQuotedMessage *_Nullable quotedMessage = [TSQuotedMessage quotedMessageForDataMessage:dataMessage thread:oldGroupThread transaction:transaction]; NSError *linkPreviewError; OWSLinkPreview *_Nullable linkPreview = [OWSLinkPreview buildValidatedLinkPreviewWithDataMessage:dataMessage body:body transaction:transaction error:&linkPreviewError]; if (linkPreviewError && ![OWSLinkPreview isNoPreviewError:linkPreviewError]) { OWSLogError(@"linkPreviewError: %@", linkPreviewError); } OWSLogDebug(@"incoming message from: %@ for group: %@ with timestamp: %lu", envelopeAddress(envelope), groupId, (unsigned long)timestamp); // Legit usage of senderTimestamp when creating an incoming group message record TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initIncomingMessageWithTimestamp:timestamp inThread:oldGroupThread authorId:hexEncodedPublicKey sourceDeviceId:envelope.sourceDevice messageBody:body attachmentIds:@[] expiresInSeconds:dataMessage.expireTimer quotedMessage:quotedMessage contactShare:contact linkPreview:linkPreview serverTimestamp:serverTimestamp wasReceivedByUD:wasReceivedByUD]; // Loki: Parse Loki specific properties if needed if (envelope.isPtpMessage) { incomingMessage.isP2P = YES; } if (dataMessage.publicChatInfo != nil && dataMessage.publicChatInfo.hasServerID) { incomingMessage.groupChatServerID = dataMessage.publicChatInfo.serverID; } NSArray *attachmentPointers = [TSAttachmentPointer attachmentPointersFromProtos:dataMessage.attachments albumMessage:incomingMessage]; for (TSAttachmentPointer *pointer in attachmentPointers) { [pointer saveWithTransaction:transaction]; [incomingMessage.attachmentIds addObject:pointer.uniqueId]; } // Loki: Don't process friend requests in group chats if (body.length == 0 && attachmentPointers.count < 1 && !contact) { OWSLogWarn(@"Ignoring empty incoming message from: %@ for group: %@ with timestamp: %lu.", hexEncodedPublicKey, groupId, (unsigned long)timestamp); return nil; } // Loki: Cache the user hex encoded public key (for mentions) dispatch_async(dispatch_get_main_queue(), ^{ [OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [LKAPI populateUserHexEncodedPublicKeyCacheIfNeededFor:oldGroupThread.uniqueId in:transaction]; [LKAPI cache:incomingMessage.authorId for:oldGroupThread.uniqueId]; }]; }); [self finalizeIncomingMessage:incomingMessage thread:oldGroupThread masterThread:oldGroupThread envelope:envelope transaction:transaction]; // Loki: Map the message ID to the message server ID if needed if (dataMessage.publicChatInfo != nil && dataMessage.publicChatInfo.hasServerID) { [self.primaryStorage setIDForMessageWithServerID:dataMessage.publicChatInfo.serverID to:incomingMessage.uniqueId in:transaction]; } return incomingMessage; } default: { OWSLogWarn(@"Ignoring unknown group message type: %d.", (int)dataMessage.group.type); return nil; } } } else { // Loki: A message from a slave device should appear as if it came from the master device; the underlying // friend request logic, however, should still be specific to the slave device. // Loki: Get the master hex encoded public key and thread NSString *hexEncodedPublicKey = envelope.source; NSString *masterHexEncodedPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source); TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction]; TSContactThread *masterThread = [TSContactThread getOrCreateThreadWithContactId:masterHexEncodedPublicKey transaction:transaction]; OWSLogDebug(@"Incoming message from: %@ with timestamp: %lu.", hexEncodedPublicKey, (unsigned long)timestamp); [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer thread:masterThread createdByRemoteRecipientId:hexEncodedPublicKey createdInExistingGroup:NO transaction:transaction]; TSQuotedMessage *_Nullable quotedMessage = [TSQuotedMessage quotedMessageForDataMessage:dataMessage thread:masterThread transaction:transaction]; NSError *linkPreviewError; OWSLinkPreview *_Nullable linkPreview = [OWSLinkPreview buildValidatedLinkPreviewWithDataMessage:dataMessage body:body transaction:transaction error:&linkPreviewError]; if (linkPreviewError && ![OWSLinkPreview isNoPreviewError:linkPreviewError]) { OWSLogError(@"linkPreviewError: %@", linkPreviewError); } // Legit usage of senderTimestamp when creating incoming message from received envelope TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initIncomingMessageWithTimestamp:timestamp inThread:masterThread authorId:masterThread.contactIdentifier sourceDeviceId:envelope.sourceDevice messageBody:body attachmentIds:@[] expiresInSeconds:dataMessage.expireTimer quotedMessage:quotedMessage contactShare:contact linkPreview:linkPreview serverTimestamp:serverTimestamp wasReceivedByUD:wasReceivedByUD]; NSString *rawDisplayName = dataMessage.profile.displayName; NSString *displayName = nil; if (rawDisplayName != nil && rawDisplayName.length > 0) { displayName = [NSString stringWithFormat:@"%@ (...%@)", rawDisplayName, [incomingMessage.authorId substringFromIndex:incomingMessage.authorId.length - 8]]; } [self.profileManager updateProfileForContactWithID:thread.contactIdentifier displayName:displayName with:transaction]; [self handleProfileKeyUpdateIfNeeded:dataMessage recipientId:thread.contactIdentifier]; // Loki: Parse Loki specific properties if needed if (envelope.isPtpMessage) { incomingMessage.isP2P = YES; } NSArray *attachmentPointers = [TSAttachmentPointer attachmentPointersFromProtos:dataMessage.attachments albumMessage:incomingMessage]; for (TSAttachmentPointer *pointer in attachmentPointers) { [pointer saveWithTransaction:transaction]; [incomingMessage.attachmentIds addObject:pointer.uniqueId]; } // Loki: Do this before the check below [self handleFriendRequestMessageIfNeededWithEnvelope:envelope data:dataMessage message:incomingMessage thread:thread transaction:transaction]; if (body.length == 0 && attachmentPointers.count < 1 && !contact) { OWSLogWarn(@"Ignoring empty incoming message from: %@ with timestamp: %lu.", hexEncodedPublicKey, (unsigned long)timestamp); return nil; } // Loki: If we received a message from a contact in the last 2 minutes that wasn't P2P, then we need to ping them. // We assume this occurred because they don't have our P2P details. if (!envelope.isPtpMessage && hexEncodedPublicKey != nil) { uint64_t timestamp = envelope.timestamp; uint64_t now = NSDate.ows_millisecondTimeStamp; uint64_t ageInSeconds = (now - timestamp) / 1000; if (ageInSeconds <= 120) { [LKP2PAPI pingContact:hexEncodedPublicKey]; } } [self finalizeIncomingMessage:incomingMessage thread:thread masterThread:thread envelope:envelope transaction:transaction]; return incomingMessage; } } - (void)handleProfileNameUpdateIfNeeded:(SSKProtoDataMessage *)dataMessage recipientId:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction { if (dataMessage != nil && dataMessage.profile != nil) { [self.profileManager updateProfileForContactWithID:recipientId displayName:dataMessage.profile.displayName with:transaction]; } } - (void)handleProfileKeyUpdateIfNeeded:(SSKProtoDataMessage *)dataMessage recipientId:(NSString *)recipientId { if (dataMessage != nil && [dataMessage hasProfileKey]) { NSData *profileKey = [dataMessage profileKey]; NSString *url = dataMessage.profile != nil ? dataMessage.profile.profilePicture : nil; if (profileKey.length == kAES256_KeyByteLength) { [self.profileManager setProfileKeyData:profileKey forRecipientId:recipientId avatarURL:url]; } else { OWSFailDebug(@"Unexpected profile key length:%lu on message from:%@", (unsigned long)profileKey.length, recipientId); } } } - (void)establishSessionsWithMembersIfNeeded:(NSArray *)members forThread:(TSGroupThread *)thread transaction:(YapDatabaseReadWriteTransaction *)transaction { NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; for (NSString *member in members) { if ([member isEqualToString:userHexEncodedPublicKey] ) { continue; } BOOL hasSession = [self.primaryStorage containsSession:member deviceId:1 protocolContext:transaction]; if (hasSession) { continue; } TSContactThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:member transaction:transaction]; LKSessionRequestMessage *message = [[LKSessionRequestMessage alloc] initWithThread:contactThread]; [self.messageSenderJobQueue addMessage:message transaction:transaction]; } } - (BOOL)canFriendRequestBeAutoAcceptedForThread:(TSContactThread *)thread transaction:(YapDatabaseReadWriteTransaction *)transaction { NSString *senderHexEncodedPublicKey = thread.contactIdentifier; if (thread.hasCurrentUserSentFriendRequest) { // This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his // mind and sent a friend request to Alice. In this case we want Alice to auto-accept the request // and send a friend request accepted message back to Bob. We don't check that sending the // friend request accepted message succeeded. Even if it doesn't, the thread's current friend // request status will be set to LKThreadFriendRequestStatusFriends for Alice making it possible // for Alice to send messages to Bob. When Bob receives a message, his thread's friend request status // will then be set to LKThreadFriendRequestStatusFriends. If we do check for a successful send // before updating Alice's thread's friend request status to LKThreadFriendRequestStatusFriends, // we can end up in a deadlock where both users' threads' friend request statuses are // LKThreadFriendRequestStatusRequestSent. return YES; } NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; NSSet *userLinkedDeviceHexEncodedPublicKeys = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userHexEncodedPublicKey in:transaction]; if ([userLinkedDeviceHexEncodedPublicKeys containsObject:senderHexEncodedPublicKey]) { // Auto-accept any friend requests from the user's own linked devices return YES; } NSSet *senderLinkedDeviceThreads = [LKDatabaseUtilities getLinkedDeviceThreadsFor:senderHexEncodedPublicKey in:transaction]; if ([senderLinkedDeviceThreads contains:^BOOL(TSContactThread *thread) { return thread.isContactFriend; }]) { // Auto-accept if the user is friends with any of the sender's linked devices. return YES; } return NO; } - (void)handleFriendRequestMessageIfNeededWithEnvelope:(SSKProtoEnvelope *)envelope data:(SSKProtoDataMessage *)data message:(TSIncomingMessage *)message thread:(TSContactThread *)thread transaction:(YapDatabaseReadWriteTransaction *)transaction { if (envelope.isGroupChatMessage) { return [LKLogger print:@"[Loki] Ignoring friend request in group chat."]; } // The envelope type is set during UD decryption. if (envelope.type != SSKProtoEnvelopeTypeFriendRequest) { return [LKLogger print:@"[Loki] Ignoring friend request logic for non friend request type envelope."]; } if ([self canFriendRequestBeAutoAcceptedForThread:thread transaction:transaction]) { [thread saveFriendRequestStatus:LKThreadFriendRequestStatusFriends withTransaction:transaction]; __block TSOutgoingMessage *existingFriendRequestMessage; [thread enumerateInteractionsWithTransaction:transaction usingBlock:^(TSInteraction *interaction, YapDatabaseReadTransaction *transaction) { if ([interaction isKindOfClass:TSOutgoingMessage.class] && ((TSOutgoingMessage *)interaction).isFriendRequest) { existingFriendRequestMessage = (TSOutgoingMessage *)interaction; } }]; if (existingFriendRequestMessage != nil) { [existingFriendRequestMessage saveFriendRequestStatus:LKMessageFriendRequestStatusAccepted withTransaction:transaction]; } // The two lines below are equivalent to calling [ThreadUtil enqueueFriendRequestAcceptanceMessageInThread:thread] LKEphemeralMessage *backgroundMessage = [[LKEphemeralMessage alloc] initInThread:thread]; [self.messageSenderJobQueue addMessage:backgroundMessage transaction:transaction]; } else if (!thread.isContactFriend) { // Checking that the sender of the message isn't already a friend is necessary because otherwise // the following situation can occur: Alice and Bob are friends. Bob loses his database and his // friend request status is reset to LKThreadFriendRequestStatusNone. Bob now sends Alice a friend // request. Alice's thread's friend request status is reset to // LKThreadFriendRequestStatusRequestReceived. [thread saveFriendRequestStatus:LKThreadFriendRequestStatusRequestReceived withTransaction:transaction]; // Except for the message.friendRequestStatus = LKMessageFriendRequestStatusPending line below, all of this is to ensure that // there's only ever one message with status LKMessageFriendRequestStatusPending in a thread (where a thread is the combination // of all threads belonging to the linked devices of a user). NSString *senderID = ((TSIncomingMessage *)message).authorId; NSSet *linkedDeviceThreads = [LKDatabaseUtilities getLinkedDeviceThreadsFor:senderID in:transaction]; for (TSContactThread *thread in linkedDeviceThreads) { [thread enumerateInteractionsWithTransaction:transaction usingBlock:^(TSInteraction *interaction, YapDatabaseReadTransaction *transaction) { if (![interaction isKindOfClass:TSIncomingMessage.class]) { return; } TSIncomingMessage *message = (TSIncomingMessage *)interaction; if (message.friendRequestStatus != LKMessageFriendRequestStatusNone) { [message saveFriendRequestStatus:LKMessageFriendRequestStatusNone withTransaction:transaction]; } }]; } message.friendRequestStatus = LKMessageFriendRequestStatusPending; // Don't save yet. This is done in finalizeIncomingMessage:thread:masterThread:envelope:transaction. } } - (void)handleFriendRequestAcceptanceIfNeededWithEnvelope:(SSKProtoEnvelope *)envelope transaction:(YapDatabaseReadWriteTransaction *)transaction { // If we get an envelope that isn't a friend request, then we can infer that we had to use // Signal cipher decryption and thus that we have a session with the other person. // The envelope type is set during UD decryption. if (envelope.isGroupChatMessage || envelope.type == SSKProtoEnvelopeTypeFriendRequest) return; // Currently this uses `envelope.source` but with sync messages we'll need to use the message sender ID TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:envelope.source transaction:transaction]; // We shouldn't be able to skip from none to friends under normal circumstances if (thread.friendRequestStatus == LKThreadFriendRequestStatusNone) { return; } // Become happy friends and go on great adventures [thread saveFriendRequestStatus:LKThreadFriendRequestStatusFriends withTransaction:transaction]; TSOutgoingMessage *existingFriendRequestMessage = [[thread getLastInteractionWithTransaction:transaction] as:TSOutgoingMessage.class]; if (existingFriendRequestMessage != nil && existingFriendRequestMessage.isFriendRequest) { [existingFriendRequestMessage saveFriendRequestStatus:LKMessageFriendRequestStatusAccepted withTransaction:transaction]; } // Send our P2P details LKAddressMessage *_Nullable onlineMessage = [LKP2PAPI onlineBroadcastMessageForThread:thread]; if (onlineMessage != nil) { [self.messageSenderJobQueue addMessage:onlineMessage transaction:transaction]; } } - (void)finalizeIncomingMessage:(TSIncomingMessage *)incomingMessage thread:(TSThread *)thread masterThread:(TSThread *)masterThread envelope:(SSKProtoEnvelope *)envelope transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return; } if (!thread) { OWSFailDebug(@"Missing thread."); return; } if (!incomingMessage) { OWSFailDebug(@"Missing incomingMessage."); return; } if (!transaction) { OWSFail(@"Missing transaction."); return; } [incomingMessage saveWithTransaction:transaction]; // Loki: Remove any old incoming messages if (incomingMessage.isFriendRequest) { [thread removeOldIncomingFriendRequestMessagesIfNeededWithTransaction:transaction]; } // Any messages sent from the current user - from this device or another - should be automatically marked as read. if ([(masterThread.contactIdentifier ?: envelope.source) isEqualToString:self.tsAccountManager.localNumber]) { // Don't send a read receipt for messages sent by ourselves. [incomingMessage markAsReadAtTimestamp:envelope.timestamp sendReadReceipt:NO transaction:transaction]; } // Download the "non-message body" attachments. NSMutableArray *otherAttachmentIds = [incomingMessage.allAttachmentIds mutableCopy]; if (incomingMessage.attachmentIds) { [otherAttachmentIds removeObjectsInArray:incomingMessage.attachmentIds]; } for (NSString *attachmentId in otherAttachmentIds) { TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction]; if (![attachment isKindOfClass:[TSAttachmentPointer class]]) { OWSLogInfo(@"Skipping attachment stream."); continue; } TSAttachmentPointer *_Nullable attachmentPointer = (TSAttachmentPointer *)attachment; OWSLogDebug(@"Downloading attachment for message: %lu", (unsigned long)incomingMessage.timestamp); // Use a separate download for each attachment so that: // // * We update the message as each comes in. // * Failures don't interfere with successes. [self.attachmentDownloads downloadAttachmentPointer:attachmentPointer success:^(NSArray *attachmentStreams) { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { TSAttachmentStream *_Nullable attachmentStream = attachmentStreams.firstObject; OWSAssertDebug(attachmentStream); if (attachmentStream && incomingMessage.quotedMessage.thumbnailAttachmentPointerId.length > 0 && [attachmentStream.uniqueId isEqualToString:incomingMessage.quotedMessage.thumbnailAttachmentPointerId]) { [incomingMessage setQuotedMessageThumbnailAttachmentStream:attachmentStream]; [incomingMessage saveWithTransaction:transaction]; } else { // We touch the message to trigger redraw of any views displaying it, // since the attachment might be a contact avatar, etc. [incomingMessage touchWithTransaction:transaction]; } }]; } failure:^(NSError *error) { OWSLogWarn(@"Failed to download attachment for message: %lu with error: %@.", (unsigned long)incomingMessage.timestamp, error); }]; } // In case we already have a read receipt for this new message (this happens sometimes). [OWSReadReceiptManager.sharedManager applyEarlyReadReceiptsForIncomingMessage:incomingMessage transaction:transaction]; // Update thread preview in inbox [masterThread touchWithTransaction:transaction]; [SSKEnvironment.shared.notificationsManager notifyUserForIncomingMessage:incomingMessage inThread:masterThread transaction:transaction]; dispatch_async(dispatch_get_main_queue(), ^{ [self.typingIndicators didReceiveIncomingMessageInThread:masterThread recipientId:(masterThread.contactIdentifier ?: envelope.source) deviceId:envelope.sourceDevice]; }); } #pragma mark - helpers - (BOOL)isDataMessageGroupAvatarUpdate:(SSKProtoDataMessage *)dataMessage { if (!dataMessage) { OWSFailDebug(@"Missing dataMessage."); return NO; } return (dataMessage.group != nil && dataMessage.group.type == SSKProtoGroupContextTypeUpdate && dataMessage.group.avatar != nil); } /** * @returns * Group or Contact thread for message, creating a new contact thread if necessary, * but never creating a new group thread. */ - (nullable TSThread *)threadForEnvelope:(SSKProtoEnvelope *)envelope dataMessage:(SSKProtoDataMessage *)dataMessage transaction:(YapDatabaseReadWriteTransaction *)transaction { if (!envelope) { OWSFailDebug(@"Missing envelope."); return nil; } if (!dataMessage) { OWSFailDebug(@"Missing dataMessage."); return nil; } if (!transaction) { OWSFail(@"Missing transaction."); return nil; } if (dataMessage.group) { NSData *groupId = dataMessage.group.id; OWSAssertDebug(groupId.length > 0); TSGroupThread *_Nullable groupThread = [TSGroupThread threadWithGroupId:groupId transaction:transaction]; // This method should only be called from a code path that has already verified // that this is a "known" group. OWSAssertDebug(groupThread); return groupThread; } else { return [TSContactThread getOrCreateThreadWithContactId:envelope.source transaction:transaction]; } } #pragma mark - - (void)checkForUnknownLinkedDevice:(SSKProtoEnvelope *)envelope transaction:(YapDatabaseReadWriteTransaction *)transaction { OWSAssertDebug(envelope); OWSAssertDebug(transaction); NSString *localNumber = self.tsAccountManager.localNumber; if (![localNumber isEqualToString:envelope.source]) { return; } // Consult the device list cache we use for message sending // whether or not we know about this linked device. SignalRecipient *_Nullable recipient = [SignalRecipient registeredRecipientForRecipientId:localNumber mustHaveDevices:NO transaction:transaction]; if (!recipient) { // OWSFailDebug(@"No local SignalRecipient."); } else { BOOL isRecipientDevice = [recipient.devices containsObject:@(envelope.sourceDevice)]; if (!isRecipientDevice) { OWSLogInfo(@"Message received from unknown linked device; adding to local SignalRecipient: %lu.", (unsigned long) envelope.sourceDevice); [recipient updateRegisteredRecipientWithDevicesToAdd:@[ @(envelope.sourceDevice) ] devicesToRemove:nil transaction:transaction]; } } // Consult the device list cache we use for the "linked device" UI // whether or not we know about this linked device. NSMutableSet *deviceIdSet = [NSMutableSet new]; for (OWSDevice *device in [OWSDevice currentDevicesWithTransaction:transaction]) { [deviceIdSet addObject:@(device.deviceId)]; } BOOL isInDeviceList = [deviceIdSet containsObject:@(envelope.sourceDevice)]; if (!isInDeviceList) { OWSLogInfo(@"Message received from unknown linked device; refreshing device list: %lu.", (unsigned long) envelope.sourceDevice); [OWSDevicesService refreshDevices]; dispatch_async(dispatch_get_main_queue(), ^{ [self.profileManager fetchLocalUsersProfile]; }); } } @end NS_ASSUME_NONNULL_END