From b2360d8e1aceab734e02cb0b255d23ed47ec937b Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 9 Feb 2024 17:02:44 +1100 Subject: [PATCH] fix an issue where DaR messages are read on linked devices won't have correct expiration start time --- Session.xcodeproj/project.pbxproj | 4 + .../DisappearingMessageConfiguration.swift | 21 ++- .../MessageReceiver+Calls.swift | 3 +- .../MessageReceiver+ExpirationTimers.swift | 37 +--- .../MessageReceiver+VisibleMessages.swift | 78 ++++----- ...MessageReceiver+DisappearingMessages.swift | 158 ++++++++++++++++++ .../Sending & Receiving/MessageReceiver.swift | 7 +- .../NotificationServiceExtension.swift | 3 +- 8 files changed, 223 insertions(+), 88 deletions(-) create mode 100644 SessionMessagingKit/Sending & Receiving/MessageReceiver+DisappearingMessages.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 644a99dc7..910ebbcad 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -161,6 +161,7 @@ 7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A892745C4F000FB91B9 /* Permissions.swift */; }; 7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A8B2747150E00FB91B9 /* TurnServerInfo.swift */; }; 7BFD1A972747689000FB91B9 /* Session-Turn-Server in Resources */ = {isa = PBXBuildFile; fileRef = 7BFD1A962747689000FB91B9 /* Session-Turn-Server */; }; + 943C6D822B75E061004ACE64 /* MessageReceiver+DisappearingMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C6D812B75E061004ACE64 /* MessageReceiver+DisappearingMessages.swift */; }; 9593A1E796C9E6BE2352EA6F /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8B0BA5257C58DC6FF797278 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework */; }; 99978E3F7A80275823CA9014 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29E827FDF6C1032BB985740C /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; }; A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; }; @@ -1307,6 +1308,7 @@ 8E946CB54A221018E23599DE /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; }; 92E8569C96285EE3CDB5960D /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 93359C81CF2660040B7CD106 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 943C6D812B75E061004ACE64 /* MessageReceiver+DisappearingMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+DisappearingMessages.swift"; sourceTree = "<group>"; }; A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; A163E8AA16F3F6A90094D68B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; A1C32D4D17A0652C000A904E /* AddressBook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBook.framework; path = System/Library/Frameworks/AddressBook.framework; sourceTree = SDKROOT; }; @@ -2833,6 +2835,7 @@ C3471ECA2555356A00297E91 /* MessageSender+Encryption.swift */, C300A5FB2554B0A000555489 /* MessageReceiver.swift */, C3471F4B25553AB000297E91 /* MessageReceiver+Decryption.swift */, + 943C6D812B75E061004ACE64 /* MessageReceiver+DisappearingMessages.swift */, ); path = "Sending & Receiving"; sourceTree = "<group>"; @@ -6092,6 +6095,7 @@ FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */, FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */, FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */, + 943C6D822B75E061004ACE64 /* MessageReceiver+DisappearingMessages.swift in Sources */, FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */, FDC13D502A16EE50007267C7 /* PushNotificationAPIEndpoint.swift in Sources */, FD432434299C6985008A0213 /* PendingReadReceipt.swift in Sources */, diff --git a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift index 0e9d7281e..cbfafa8aa 100644 --- a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift +++ b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift @@ -220,6 +220,7 @@ public extension DisappearingMessagesConfiguration { authorId: String, timestampMs: Int64, serverHash: String?, + serverExpirationTimestamp: TimeInterval?, updatedConfiguration: DisappearingMessagesConfiguration, using dependencies: Dependencies = Dependencies() ) throws -> Int64? { @@ -252,7 +253,13 @@ public extension DisappearingMessagesConfiguration { openGroup: nil ) ) - let expiresStartedAtMs: Double? = (updatedConfiguration.type == .disappearAfterSend || wasRead) ? Double(timestampMs) : nil + let messageExpirationInfo: MessageReceiver.MessageExpirationInfo = MessageReceiver.getMessageExpirationInfo( + db, + wasRead: wasRead, + serverExpirationTimestamp: serverExpirationTimestamp, + expiresInSeconds: (updatedConfiguration.type == .disappearAfterSend) ? Double(timestampMs) : nil, + expiresStartedAtMs: updatedConfiguration.durationSeconds + ) let interaction = try Interaction( serverHash: serverHash, threadId: threadId, @@ -265,9 +272,19 @@ public extension DisappearingMessagesConfiguration { timestampMs: timestampMs, wasRead: wasRead, expiresInSeconds: (threadVariant == .legacyGroup ? nil : updatedConfiguration.durationSeconds), // Do not expire this control message in legacy groups - expiresStartedAtMs: (threadVariant == .legacyGroup ? nil : expiresStartedAtMs) + expiresStartedAtMs: (threadVariant == .legacyGroup ? nil : messageExpirationInfo.expiresStartedAtMs) ).inserted(db) + if messageExpirationInfo.shouldUpdateExpiry { + MessageReceiver.updateExpiryForDisappearAfterReadMessages( + db, + threadId: threadId, + serverHash: serverHash, + expiresInSeconds: messageExpirationInfo.expiresInSeconds, + expiresStartedAtMs: messageExpirationInfo.expiresStartedAtMs + ) + } + return interaction.id } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index 0c0fdb9c0..2c1f8d07d 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -11,7 +11,8 @@ extension MessageReceiver { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: CallMessage + message: CallMessage, + serverExpirationTimestamp: TimeInterval? ) throws { // Only support calls from contact threads guard threadVariant == .contact else { return } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift index bb1c0c245..90cd48c75 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift @@ -131,47 +131,12 @@ extension MessageReceiver { ).inserted(db) } - public static func updateContactDisappearingMessagesVersionIfNeeded( - _ db: Database, - messageVariant: Message.Variant?, - contactId: String?, - version: FeatureVersion? - ) { - guard - let messageVariant: Message.Variant = messageVariant, - let contactId: String = contactId, - let version: FeatureVersion = version - else { - return - } - - guard [ .visibleMessage, .expirationTimerUpdate ].contains(messageVariant) else { return } - - _ = try? Contact - .filter(id: contactId) - .updateAllAndConfig( - db, - Contact.Columns.lastKnownClientVersion.set(to: version) - ) - - guard Features.useNewDisappearingMessagesConfig else { return } - - if contactId == getUserHexEncodedPublicKey(db) { - switch version { - case .legacyDisappearingMessages: - TopBannerController.show(warning: .outdatedUserConfig) - case .newDisappearingMessages: - TopBannerController.hide() - } - } - - } - internal static func handleExpirationTimerUpdate( _ db: Database, threadId: String, threadVariant: SessionThread.Variant, message: ExpirationTimerUpdate, + serverExpirationTimestamp: TimeInterval?, proto: SNProtoContent ) throws { guard proto.hasExpirationType || proto.hasExpirationTimer else { return } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 2d1d2edf0..cac4b1cc5 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -12,6 +12,7 @@ extension MessageReceiver { threadId: String, threadVariant: SessionThread.Variant, message: VisibleMessage, + serverExpirationTimestamp: TimeInterval?, associatedWithProto proto: SNProtoContent, using dependencies: Dependencies = Dependencies() ) throws -> Int64 { @@ -139,7 +140,24 @@ extension MessageReceiver { // prevent the ability to insert duplicate interactions at a database level // so we don't need to check for the existance of a message beforehand anymore let interaction: Interaction - + // Auto-mark sent messages or messages older than the 'lastReadTimestampMs' as read + let wasRead: Bool = ( + variant == .standardOutgoing || + SessionUtil.timestampAlreadyRead( + threadId: thread.id, + threadVariant: thread.variant, + timestampMs: Int64(messageSentTimestamp * 1000), + userPublicKey: currentUserPublicKey, + openGroup: maybeOpenGroup + ) + ) + let messageExpirationInfo: MessageExpirationInfo = getMessageExpirationInfo( + db, + wasRead: wasRead, + serverExpirationTimestamp: serverExpirationTimestamp, + expiresInSeconds: message.expiresInSeconds, + expiresStartedAtMs: message.expiresStartedAtMs + ) do { interaction = try Interaction( serverHash: message.serverHash, // Keep track of server hash @@ -148,17 +166,7 @@ extension MessageReceiver { variant: variant, body: message.text, timestampMs: Int64(messageSentTimestamp * 1000), - wasRead: ( - // Auto-mark sent messages or messages older than the 'lastReadTimestampMs' as read - variant == .standardOutgoing || - SessionUtil.timestampAlreadyRead( - threadId: thread.id, - threadVariant: thread.variant, - timestampMs: Int64(messageSentTimestamp * 1000), - userPublicKey: currentUserPublicKey, - openGroup: maybeOpenGroup - ) - ), + wasRead: wasRead, hasMention: Interaction.isUserMentioned( db, threadId: thread.id, @@ -166,8 +174,8 @@ extension MessageReceiver { quoteAuthorId: dataMessage.quote?.author, using: dependencies ), - expiresInSeconds: message.expiresInSeconds, - expiresStartedAtMs: message.expiresStartedAtMs, + expiresInSeconds: messageExpirationInfo.expiresInSeconds, + expiresStartedAtMs: messageExpirationInfo.expiresStartedAtMs, // OpenGroupInvitations are stored as LinkPreview's in the database linkPreviewUrl: (message.linkPreview?.url ?? message.openGroupInvitation?.url), // Keep track of the open group server message ID ↔ message ID relationship @@ -235,6 +243,16 @@ extension MessageReceiver { syncTarget: message.syncTarget ) + if messageExpirationInfo.shouldUpdateExpiry { + updateExpiryForDisappearAfterReadMessages( + db, + threadId: threadId, + serverHash: message.serverHash, + expiresInSeconds: messageExpirationInfo.expiresInSeconds, + expiresStartedAtMs: messageExpirationInfo.expiresStartedAtMs + ) + } + getExpirationForOutgoingDisappearingMessages( db, threadId: threadId, @@ -511,36 +529,4 @@ extension MessageReceiver { _ = try pendingReadReceipt.delete(db) } } - - private static func getExpirationForOutgoingDisappearingMessages( - _ db: Database, - threadId: String, - variant: Interaction.Variant, - serverHash: String?, - expireInSeconds: TimeInterval? - ) { - guard - variant == .standardOutgoing, - let serverHash: String = serverHash, - let expireInSeconds: TimeInterval = expireInSeconds, - expireInSeconds > 0 - else { - return - } - - let startedAtTimestampMs: Double = Double(SnodeAPI.currentOffsetTimestampMs()) - - JobRunner.add( - db, - job: Job( - variant: .getExpiration, - behaviour: .runOnce, - threadId: threadId, - details: GetExpirationJob.Details( - expirationInfo: [serverHash: expireInSeconds], - startedAtTimestampMs: startedAtTimestampMs - ) - ) - ) - } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+DisappearingMessages.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+DisappearingMessages.swift new file mode 100644 index 000000000..04fe294a0 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+DisappearingMessages.swift @@ -0,0 +1,158 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionSnodeKit +import SessionUIKit +import SessionUtilitiesKit + +extension MessageReceiver { + public static func updateContactDisappearingMessagesVersionIfNeeded( + _ db: Database, + messageVariant: Message.Variant?, + contactId: String?, + version: FeatureVersion? + ) { + guard + let messageVariant: Message.Variant = messageVariant, + let contactId: String = contactId, + let version: FeatureVersion = version + else { + return + } + + guard [ .visibleMessage, .expirationTimerUpdate ].contains(messageVariant) else { return } + + _ = try? Contact + .filter(id: contactId) + .updateAllAndConfig( + db, + Contact.Columns.lastKnownClientVersion.set(to: version) + ) + + guard Features.useNewDisappearingMessagesConfig else { return } + + if contactId == getUserHexEncodedPublicKey(db) { + switch version { + case .legacyDisappearingMessages: + TopBannerController.show(warning: .outdatedUserConfig) + case .newDisappearingMessages: + TopBannerController.hide() + } + } + } + + public struct MessageExpirationInfo { + let expiresStartedAtMs: Double? + let expiresInSeconds: TimeInterval? + let shouldUpdateExpiry: Bool + } + + public static func getMessageExpirationInfo( + _ db: Database, + wasRead: Bool, + serverExpirationTimestamp: TimeInterval?, + expiresInSeconds: TimeInterval?, + expiresStartedAtMs: Double?, + using dependencies: Dependencies = Dependencies() + ) -> MessageExpirationInfo { + var shouldUpdateExpiry: Bool = false + let expiresStartedAtMs: Double? = { + // Disappear after sent + guard expiresStartedAtMs == nil else { + return expiresStartedAtMs + } + + // Disappear after read + guard + let expiresInSeconds: TimeInterval = expiresInSeconds, + expiresInSeconds > 0, + wasRead, + let serverExpirationTimestamp: TimeInterval = serverExpirationTimestamp + else { + return nil + } + + let nowMs: Double = Double(SnodeAPI.currentOffsetTimestampMs()) + let serverExpirationTimestampMs: Double = serverExpirationTimestamp * 1000 + let expiresInMs: Double = expiresInSeconds * 1000 + + if serverExpirationTimestampMs <= (nowMs + expiresInMs) { + // seems to have been shortened already + return (serverExpirationTimestampMs - expiresInMs) + } else { + // consider that message unread + shouldUpdateExpiry = true + return (nowMs + expiresInSeconds) + } + }() + + return MessageExpirationInfo( + expiresStartedAtMs: expiresStartedAtMs, + expiresInSeconds: expiresInSeconds, + shouldUpdateExpiry: shouldUpdateExpiry + ) + } + + public static func getExpirationForOutgoingDisappearingMessages( + _ db: Database, + threadId: String, + variant: Interaction.Variant, + serverHash: String?, + expireInSeconds: TimeInterval? + ) { + guard + variant == .standardOutgoing, + let serverHash: String = serverHash, + let expireInSeconds: TimeInterval = expireInSeconds, + expireInSeconds > 0 + else { + return + } + + let startedAtTimestampMs: Double = Double(SnodeAPI.currentOffsetTimestampMs()) + + JobRunner.add( + db, + job: Job( + variant: .getExpiration, + behaviour: .runOnce, + threadId: threadId, + details: GetExpirationJob.Details( + expirationInfo: [serverHash: expireInSeconds], + startedAtTimestampMs: startedAtTimestampMs + ) + ) + ) + } + + public static func updateExpiryForDisappearAfterReadMessages( + _ db: Database, + threadId: String, + serverHash: String?, + expiresInSeconds: TimeInterval?, + expiresStartedAtMs: Double? + ) { + guard + let serverHash: String = serverHash, + let expiresInSeconds: TimeInterval = expiresInSeconds, + let expiresStartedAtMs: Double = expiresStartedAtMs + else { + return + } + + let expirationTimestampMs: Int64 = Int64(expiresStartedAtMs + expiresInSeconds * 1000) + JobRunner.add( + db, + job: Job( + variant: .expirationUpdate, + behaviour: .runOnce, + threadId: threadId, + details: ExpirationUpdateJob.Details( + serverHashes: [serverHash], + expirationTimestampMs: expirationTimestampMs + ) + ) + ) + } +} diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 2b08f0515..79895d953 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -264,6 +264,7 @@ public enum MessageReceiver { threadId: threadId, threadVariant: threadVariant, message: message, + serverExpirationTimestamp: serverExpirationTimestamp, proto: proto ) @@ -280,7 +281,8 @@ public enum MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message + message: message, + serverExpirationTimestamp: serverExpirationTimestamp ) case let message as MessageRequestResponse: @@ -295,7 +297,8 @@ public enum MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message, + message: message, + serverExpirationTimestamp: serverExpirationTimestamp, associatedWithProto: proto ) diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 97970dc86..e6e8d7eb2 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -135,7 +135,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension db, threadId: processedMessage.threadId, threadVariant: processedMessage.threadVariant, - message: callMessage + message: callMessage, + serverExpirationTimestamp: processedMessage.messageInfo.serverExpirationTimestamp ) guard case .preOffer = callMessage.kind else { return self.completeSilenty() }