mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Fixed an issue where read receipts could be sent for already read messages Fixed an issue where the read state change might not update the UI
391 lines
16 KiB
Swift
391 lines
16 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import GRDB
|
|
import Sodium
|
|
import SignalCoreKit
|
|
import SessionUtilitiesKit
|
|
import SessionSnodeKit
|
|
|
|
public enum MessageReceiver {
|
|
private static var lastEncryptionKeyPairRequest: [String: Date] = [:]
|
|
|
|
public static func parse(
|
|
_ db: Database,
|
|
envelope: SNProtoEnvelope,
|
|
serverExpirationTimestamp: TimeInterval?,
|
|
openGroupId: String?,
|
|
openGroupMessageServerId: Int64?,
|
|
openGroupServerPublicKey: String?,
|
|
isOutgoing: Bool? = nil,
|
|
otherBlindedPublicKey: String? = nil,
|
|
dependencies: SMKDependencies = SMKDependencies()
|
|
) throws -> (Message, SNProtoContent, String) {
|
|
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
|
|
let isOpenGroupMessage: Bool = (openGroupId != nil)
|
|
|
|
// Decrypt the contents
|
|
guard let ciphertext = envelope.content else { throw MessageReceiverError.noData }
|
|
|
|
var plaintext: Data
|
|
var sender: String
|
|
var groupPublicKey: String? = nil
|
|
|
|
if isOpenGroupMessage {
|
|
(plaintext, sender) = (envelope.content!, envelope.source!)
|
|
}
|
|
else {
|
|
switch envelope.type {
|
|
case .sessionMessage:
|
|
// Default to 'standard' as the old code didn't seem to require an `envelope.source`
|
|
switch (SessionId.Prefix(from: envelope.source) ?? .standard) {
|
|
case .standard, .unblinded:
|
|
guard let userX25519KeyPair: Box.KeyPair = Identity.fetchUserKeyPair(db) else {
|
|
throw MessageReceiverError.noUserX25519KeyPair
|
|
}
|
|
|
|
(plaintext, sender) = try decryptWithSessionProtocol(ciphertext: ciphertext, using: userX25519KeyPair)
|
|
|
|
case .blinded:
|
|
guard let otherBlindedPublicKey: String = otherBlindedPublicKey else {
|
|
throw MessageReceiverError.noData
|
|
}
|
|
guard let openGroupServerPublicKey: String = openGroupServerPublicKey else {
|
|
throw MessageReceiverError.invalidGroupPublicKey
|
|
}
|
|
guard let userEd25519KeyPair: Box.KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
|
|
throw MessageReceiverError.noUserED25519KeyPair
|
|
}
|
|
|
|
(plaintext, sender) = try decryptWithSessionBlindingProtocol(
|
|
data: ciphertext,
|
|
isOutgoing: (isOutgoing == true),
|
|
otherBlindedPublicKey: otherBlindedPublicKey,
|
|
with: openGroupServerPublicKey,
|
|
userEd25519KeyPair: userEd25519KeyPair,
|
|
using: dependencies
|
|
)
|
|
}
|
|
|
|
case .closedGroupMessage:
|
|
guard
|
|
let hexEncodedGroupPublicKey = envelope.source,
|
|
let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: hexEncodedGroupPublicKey)
|
|
else {
|
|
throw MessageReceiverError.invalidGroupPublicKey
|
|
}
|
|
guard
|
|
let encryptionKeyPairs: [ClosedGroupKeyPair] = try? closedGroup.keyPairs.order(ClosedGroupKeyPair.Columns.receivedTimestamp.desc).fetchAll(db),
|
|
!encryptionKeyPairs.isEmpty
|
|
else {
|
|
throw MessageReceiverError.noGroupKeyPair
|
|
}
|
|
|
|
// Loop through all known group key pairs in reverse order (i.e. try the latest key
|
|
// pair first (which'll more than likely be the one we want) but try older ones in
|
|
// case that didn't work)
|
|
func decrypt(keyPairs: [ClosedGroupKeyPair], lastError: Error? = nil) throws -> (Data, String) {
|
|
guard let keyPair: ClosedGroupKeyPair = keyPairs.first else {
|
|
throw (lastError ?? MessageReceiverError.decryptionFailed)
|
|
}
|
|
|
|
do {
|
|
return try decryptWithSessionProtocol(
|
|
ciphertext: ciphertext,
|
|
using: Box.KeyPair(
|
|
publicKey: keyPair.publicKey.bytes,
|
|
secretKey: keyPair.secretKey.bytes
|
|
)
|
|
)
|
|
}
|
|
catch {
|
|
return try decrypt(keyPairs: Array(keyPairs.suffix(from: 1)), lastError: error)
|
|
}
|
|
}
|
|
|
|
groupPublicKey = hexEncodedGroupPublicKey
|
|
(plaintext, sender) = try decrypt(keyPairs: encryptionKeyPairs)
|
|
|
|
default: throw MessageReceiverError.unknownEnvelopeType
|
|
}
|
|
}
|
|
|
|
// Don't process the envelope any further if the sender is blocked
|
|
guard (try? Contact.fetchOne(db, id: sender))?.isBlocked != true else {
|
|
throw MessageReceiverError.senderBlocked
|
|
}
|
|
|
|
// Parse the proto
|
|
let proto: SNProtoContent
|
|
|
|
do {
|
|
proto = try SNProtoContent.parseData(plaintext.removePadding())
|
|
}
|
|
catch {
|
|
SNLog("Couldn't parse proto due to error: \(error).")
|
|
throw error
|
|
}
|
|
|
|
// Parse the message
|
|
guard let message: Message = Message.createMessageFrom(proto, sender: sender) else {
|
|
throw MessageReceiverError.unknownMessage
|
|
}
|
|
|
|
// Ignore self sends if needed
|
|
guard message.isSelfSendValid || sender != userPublicKey else {
|
|
throw MessageReceiverError.selfSend
|
|
}
|
|
|
|
// Guard against control messages in open groups
|
|
guard !isOpenGroupMessage || message is VisibleMessage else {
|
|
throw MessageReceiverError.invalidMessage
|
|
}
|
|
|
|
// Finish parsing
|
|
message.sender = sender
|
|
message.recipient = userPublicKey
|
|
message.sentTimestamp = envelope.timestamp
|
|
message.receivedTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
|
|
message.groupPublicKey = groupPublicKey
|
|
message.openGroupServerMessageId = openGroupMessageServerId.map { UInt64($0) }
|
|
|
|
// Validate
|
|
var isValid: Bool = message.isValid
|
|
if message is VisibleMessage && !isValid && proto.dataMessage?.attachments.isEmpty == false {
|
|
isValid = true
|
|
}
|
|
|
|
guard isValid else {
|
|
throw MessageReceiverError.invalidMessage
|
|
}
|
|
|
|
// Extract the proper threadId for the message
|
|
let threadId: String = {
|
|
if let groupPublicKey: String = groupPublicKey { return groupPublicKey }
|
|
if let openGroupId: String = openGroupId { return openGroupId }
|
|
|
|
switch message {
|
|
case let message as VisibleMessage: return (message.syncTarget ?? sender)
|
|
case let message as ExpirationTimerUpdate: return (message.syncTarget ?? sender)
|
|
default: return sender
|
|
}
|
|
}()
|
|
|
|
return (message, proto, threadId)
|
|
}
|
|
|
|
// MARK: - Handling
|
|
|
|
public static func handle(
|
|
_ db: Database,
|
|
message: Message,
|
|
serverExpirationTimestamp: TimeInterval?,
|
|
associatedWithProto proto: SNProtoContent,
|
|
openGroupId: String?,
|
|
dependencies: SMKDependencies = SMKDependencies()
|
|
) throws {
|
|
switch message {
|
|
case let message as ReadReceipt:
|
|
try MessageReceiver.handleReadReceipt(
|
|
db,
|
|
message: message,
|
|
serverExpirationTimestamp: serverExpirationTimestamp
|
|
)
|
|
|
|
case let message as TypingIndicator:
|
|
try MessageReceiver.handleTypingIndicator(db, message: message)
|
|
|
|
case let message as ClosedGroupControlMessage:
|
|
try MessageReceiver.handleClosedGroupControlMessage(db, message)
|
|
|
|
case let message as DataExtractionNotification:
|
|
try MessageReceiver.handleDataExtractionNotification(db, message: message)
|
|
|
|
case let message as ExpirationTimerUpdate:
|
|
try MessageReceiver.handleExpirationTimerUpdate(db, message: message)
|
|
|
|
case let message as ConfigurationMessage:
|
|
try MessageReceiver.handleConfigurationMessage(db, message: message)
|
|
|
|
case let message as UnsendRequest:
|
|
try MessageReceiver.handleUnsendRequest(db, message: message)
|
|
|
|
case let message as CallMessage:
|
|
try MessageReceiver.handleCallMessage(db, message: message)
|
|
|
|
case let message as MessageRequestResponse:
|
|
try MessageReceiver.handleMessageRequestResponse(db, message: message, dependencies: dependencies)
|
|
|
|
case let message as VisibleMessage:
|
|
try MessageReceiver.handleVisibleMessage(
|
|
db,
|
|
message: message,
|
|
associatedWithProto: proto,
|
|
openGroupId: openGroupId
|
|
)
|
|
|
|
default: fatalError()
|
|
}
|
|
|
|
// Perform any required post-handling logic
|
|
try MessageReceiver.postHandleMessage(db, message: message, openGroupId: openGroupId)
|
|
}
|
|
|
|
public static func postHandleMessage(
|
|
_ db: Database,
|
|
message: Message,
|
|
openGroupId: String?
|
|
) throws {
|
|
// When handling any non-typing indicator message we want to make sure the thread becomes
|
|
// visible (the only other spot this flag gets set is when sending messages)
|
|
switch message {
|
|
case is TypingIndicator: break
|
|
|
|
default:
|
|
guard let threadInfo: (id: String, variant: SessionThread.Variant) = threadInfo(db, message: message, openGroupId: openGroupId) else {
|
|
return
|
|
}
|
|
|
|
_ = try SessionThread
|
|
.fetchOrCreate(db, id: threadInfo.id, variant: threadInfo.variant)
|
|
.with(shouldBeVisible: true)
|
|
.saved(db)
|
|
}
|
|
}
|
|
|
|
public static func handleOpenGroupReactions(
|
|
_ db: Database,
|
|
threadId: String,
|
|
openGroupMessageServerId: Int64,
|
|
openGroupReactions: [Reaction]
|
|
) throws {
|
|
guard let interactionId: Int64 = try? Interaction
|
|
.select(.id)
|
|
.filter(Interaction.Columns.threadId == threadId)
|
|
.filter(Interaction.Columns.openGroupServerMessageId == openGroupMessageServerId)
|
|
.asRequest(of: Int64.self)
|
|
.fetchOne(db)
|
|
else {
|
|
throw MessageReceiverError.invalidMessage
|
|
}
|
|
|
|
_ = try Reaction
|
|
.filter(Reaction.Columns.interactionId == interactionId)
|
|
.deleteAll(db)
|
|
|
|
for reaction in openGroupReactions {
|
|
try reaction.with(interactionId: interactionId).insert(db)
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
internal static func threadInfo(_ db: Database, message: Message, openGroupId: String?) -> (id: String, variant: SessionThread.Variant)? {
|
|
if let openGroupId: String = openGroupId {
|
|
// Note: We don't want to create a thread for an open group if it doesn't exist
|
|
if (try? SessionThread.exists(db, id: openGroupId)) != true { return nil }
|
|
|
|
return (openGroupId, .openGroup)
|
|
}
|
|
|
|
if let groupPublicKey: String = message.groupPublicKey {
|
|
// Note: We don't want to create a thread for a closed group if it doesn't exist
|
|
if (try? SessionThread.exists(db, id: groupPublicKey)) != true { return nil }
|
|
|
|
return (groupPublicKey, .closedGroup)
|
|
}
|
|
|
|
// Extract the 'syncTarget' value if there is one
|
|
let maybeSyncTarget: String?
|
|
|
|
switch message {
|
|
case let message as VisibleMessage: maybeSyncTarget = message.syncTarget
|
|
case let message as ExpirationTimerUpdate: maybeSyncTarget = message.syncTarget
|
|
default: maybeSyncTarget = nil
|
|
}
|
|
|
|
// Note: We don't want to create a thread for a closed group if it doesn't exist
|
|
guard let contactId: String = (maybeSyncTarget ?? message.sender) else { return nil }
|
|
|
|
return (contactId, .contact)
|
|
}
|
|
|
|
internal static func updateProfileIfNeeded(
|
|
_ db: Database,
|
|
publicKey: String,
|
|
name: String?,
|
|
profilePictureUrl: String?,
|
|
profileKey: OWSAES256Key?,
|
|
sentTimestamp: TimeInterval,
|
|
dependencies: Dependencies = Dependencies()
|
|
) throws {
|
|
let isCurrentUser = (publicKey == getUserHexEncodedPublicKey(db, dependencies: dependencies))
|
|
let profile: Profile = Profile.fetchOrCreate(id: publicKey)
|
|
var updatedProfile: Profile = profile
|
|
|
|
// Name
|
|
if let name = name, !name.isEmpty, name != profile.name {
|
|
let shouldUpdate: Bool
|
|
if isCurrentUser {
|
|
shouldUpdate = given(UserDefaults.standard[.lastDisplayNameUpdate]) {
|
|
sentTimestamp > $0.timeIntervalSince1970
|
|
}
|
|
.defaulting(to: true)
|
|
}
|
|
else {
|
|
shouldUpdate = true
|
|
}
|
|
|
|
if shouldUpdate {
|
|
if isCurrentUser {
|
|
UserDefaults.standard[.lastDisplayNameUpdate] = Date(timeIntervalSince1970: sentTimestamp)
|
|
}
|
|
|
|
updatedProfile = updatedProfile.with(name: name)
|
|
}
|
|
}
|
|
|
|
// Profile picture & profile key
|
|
if
|
|
let profileKey: OWSAES256Key = profileKey,
|
|
let profilePictureUrl: String = profilePictureUrl,
|
|
profileKey.keyData.count == kAES256_KeyByteLength,
|
|
profileKey != profile.profileEncryptionKey
|
|
{
|
|
let shouldUpdate: Bool
|
|
if isCurrentUser {
|
|
shouldUpdate = given(UserDefaults.standard[.lastProfilePictureUpdate]) {
|
|
sentTimestamp > $0.timeIntervalSince1970
|
|
}
|
|
.defaulting(to: true)
|
|
}
|
|
else {
|
|
shouldUpdate = true
|
|
}
|
|
|
|
if shouldUpdate {
|
|
if isCurrentUser {
|
|
UserDefaults.standard[.lastProfilePictureUpdate] = Date(timeIntervalSince1970: sentTimestamp)
|
|
}
|
|
|
|
updatedProfile = updatedProfile.with(
|
|
profilePictureUrl: .update(profilePictureUrl),
|
|
profileEncryptionKey: .update(profileKey)
|
|
)
|
|
}
|
|
}
|
|
|
|
// Persist any changes
|
|
if updatedProfile != profile {
|
|
try updatedProfile.save(db)
|
|
}
|
|
|
|
// Download the profile picture if needed
|
|
if updatedProfile.profilePictureUrl != profile.profilePictureUrl || updatedProfile.profileEncryptionKey != profile.profileEncryptionKey {
|
|
db.afterNextTransaction { _ in
|
|
ProfileManager.downloadAvatar(for: updatedProfile)
|
|
}
|
|
}
|
|
}
|
|
}
|