session-ios/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Configurati...

178 lines
8.6 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import SignalCoreKit
import SessionUtilitiesKit
extension MessageReceiver {
internal static func handleConfigurationMessage(_ db: Database, message: ConfigurationMessage) throws {
let userPublicKey = getUserHexEncodedPublicKey(db)
guard message.sender == userPublicKey else { return }
SNLog("Configuration message received.")
// Note: `message.sentTimestamp` is in ms (convert to TimeInterval before converting to
// seconds to maintain the accuracy)
let isInitialSync: Bool = (!UserDefaults.standard[.hasSyncedInitialConfiguration])
let messageSentTimestamp: TimeInterval = TimeInterval((message.sentTimestamp ?? 0) / 1000)
let lastConfigTimestamp: TimeInterval = UserDefaults.standard[.lastConfigurationSync]
.defaulting(to: Date(timeIntervalSince1970: 0))
.timeIntervalSince1970
// Profile (also force-approve the current user in case the account got into a weird state or
// restored directly from a migration)
try MessageReceiver.updateProfileIfNeeded(
db,
publicKey: userPublicKey,
name: message.displayName,
profilePictureUrl: message.profilePictureUrl,
profileKey: OWSAES256Key(data: message.profileKey),
sentTimestamp: messageSentTimestamp
)
try Contact(id: userPublicKey)
.with(
isApproved: true,
didApproveMe: true
)
.save(db)
if isInitialSync || messageSentTimestamp > lastConfigTimestamp {
if isInitialSync {
UserDefaults.standard[.hasSyncedInitialConfiguration] = true
NotificationCenter.default.post(name: .initialConfigurationMessageReceived, object: nil)
}
UserDefaults.standard[.lastConfigurationSync] = Date(timeIntervalSince1970: messageSentTimestamp)
// Contacts
try message.contacts.forEach { contactInfo in
guard let sessionId: String = contactInfo.publicKey else { return }
// If the contact is a blinded contact then only add them if they haven't already been
// unblinded
if SessionId.Prefix(from: sessionId) == .blinded {
let hasUnblindedContact: Bool = (try? BlindedIdLookup
.filter(BlindedIdLookup.Columns.blindedId == sessionId)
.filter(BlindedIdLookup.Columns.sessionId != nil)
.isNotEmpty(db))
.defaulting(to: false)
if hasUnblindedContact {
return
}
}
// Note: We only update the contact and profile records if the data has actually changed
// in order to avoid triggering UI updates for every thread on the home screen
let contact: Contact = Contact.fetchOrCreate(db, id: sessionId)
let profile: Profile = Profile.fetchOrCreate(db, id: sessionId)
if
profile.name != contactInfo.displayName ||
profile.profilePictureUrl != contactInfo.profilePictureUrl ||
profile.profileEncryptionKey != contactInfo.profileKey.map({ OWSAES256Key(data: $0) })
{
try profile
.with(
name: contactInfo.displayName,
profilePictureUrl: .updateIf(contactInfo.profilePictureUrl),
profileEncryptionKey: .updateIf(
contactInfo.profileKey.map { OWSAES256Key(data: $0) }
)
)
.save(db)
}
/// We only update these values if the proto actually has values for them (this is to prevent an
/// edge case where an old client could override the values with default values since they aren't included)
///
/// **Note:** Since message requests have no reverse, we should only handle setting `isApproved`
/// and `didApproveMe` to `true`. This may prevent some weird edge cases where a config message
/// swapping `isApproved` and `didApproveMe` to `false`
if
(contactInfo.hasIsApproved && (contact.isApproved != contactInfo.isApproved)) ||
(contactInfo.hasIsBlocked && (contact.isBlocked != contactInfo.isBlocked)) ||
(contactInfo.hasDidApproveMe && (contact.didApproveMe != contactInfo.didApproveMe))
{
try contact
.with(
isApproved: (contactInfo.hasIsApproved && contactInfo.isApproved ?
true :
.existing
),
isBlocked: (contactInfo.hasIsBlocked ?
.update(contactInfo.isBlocked) :
.existing
),
didApproveMe: (contactInfo.hasDidApproveMe && contactInfo.didApproveMe ?
true :
.existing
)
)
.save(db)
}
// If the contact is blocked
if contactInfo.hasIsBlocked && contactInfo.isBlocked {
// If this message changed them to the blocked state and there is an existing thread
// associated with them that is a message request thread then delete it (assume
// that the current user had deleted that message request)
if
contactInfo.isBlocked != contact.isBlocked, // 'contact.isBlocked' will be the old value
let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId),
thread.isMessageRequest(db)
{
_ = try thread.delete(db)
}
}
}
// Closed groups
//
// Note: Only want to add these for initial sync to avoid re-adding closed groups the user
// intentionally left (any closed groups joined since the first processed sync message should
// get added via the 'handleNewClosedGroup' method anyway as they will have come through in the
// past two weeks)
if isInitialSync {
let existingClosedGroupsIds: [String] = (try? SessionThread
.filter(SessionThread.Columns.variant == SessionThread.Variant.closedGroup)
.fetchAll(db))
.defaulting(to: [])
.map { $0.id }
try message.closedGroups.forEach { closedGroup in
guard !existingClosedGroupsIds.contains(closedGroup.publicKey) else { return }
let keyPair: Box.KeyPair = Box.KeyPair(
publicKey: closedGroup.encryptionKeyPublicKey.bytes,
secretKey: closedGroup.encryptionKeySecretKey.bytes
)
try MessageReceiver.handleNewClosedGroup(
db,
groupPublicKey: closedGroup.publicKey,
name: closedGroup.name,
encryptionKeyPair: keyPair,
members: [String](closedGroup.members),
admins: [String](closedGroup.admins),
expirationTimer: closedGroup.expirationTimer,
messageSentTimestamp: message.sentTimestamp!
)
}
}
// Open groups
for openGroupURL in message.openGroups {
if let (room, server, publicKey) = OpenGroupManager.parseOpenGroup(from: openGroupURL) {
OpenGroupManager.shared
.add(db, roomToken: room, server: server, publicKey: publicKey, isConfigMessage: true)
.retainUntilComplete()
}
}
}
}
}