2022-04-01 08:22:45 +02:00
|
|
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
|
|
|
|
import Foundation
|
2022-04-06 07:43:26 +02:00
|
|
|
import GRDB
|
|
|
|
import Sodium
|
2022-04-01 08:22:45 +02:00
|
|
|
import Curve25519Kit
|
2020-11-25 06:15:16 +01:00
|
|
|
import SignalCoreKit
|
2021-08-04 07:11:49 +02:00
|
|
|
import SessionSnodeKit
|
2020-11-18 05:53:45 +01:00
|
|
|
|
2020-11-25 06:15:16 +01:00
|
|
|
extension MessageReceiver {
|
2020-11-18 05:53:45 +01:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
public static func handle(_ db: Database, message: Message, associatedWithProto proto: SNProtoContent, openGroupId: String?, isBackgroundPoll: Bool) throws {
|
2020-11-25 06:15:16 +01:00
|
|
|
switch message {
|
2022-04-21 08:42:35 +02:00
|
|
|
case let message as ReadReceipt: try handleReadReceipt(db, message: message)
|
|
|
|
case let message as TypingIndicator: try handleTypingIndicator(db, message: message)
|
|
|
|
case let message as ClosedGroupControlMessage: try handleClosedGroupControlMessage(db, message)
|
|
|
|
|
|
|
|
case let message as DataExtractionNotification:
|
|
|
|
try handleDataExtractionNotification(db, message: message)
|
|
|
|
|
|
|
|
case let message as ExpirationTimerUpdate: try handleExpirationTimerUpdate(db, message: message)
|
|
|
|
case let message as ConfigurationMessage: try handleConfigurationMessage(db, message)
|
|
|
|
case let message as UnsendRequest: try handleUnsendRequest(db, message: message)
|
|
|
|
case let message as MessageRequestResponse: try handleMessageRequestResponse(db, message)
|
|
|
|
|
|
|
|
case let message as VisibleMessage:
|
|
|
|
try handleVisibleMessage(db, message: message, associatedWithProto: proto, openGroupId: openGroupId, isBackgroundPoll: isBackgroundPoll)
|
|
|
|
|
|
|
|
default: fatalError()
|
|
|
|
}
|
|
|
|
|
2022-05-06 04:44:26 +02:00
|
|
|
// 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)
|
|
|
|
}
|
2020-11-18 05:53:45 +01:00
|
|
|
}
|
2021-02-25 04:06:40 +01:00
|
|
|
|
2022-05-06 04:44:26 +02:00
|
|
|
// MARK: - Convenience
|
|
|
|
|
|
|
|
private 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)
|
|
|
|
}
|
2021-02-25 04:06:40 +01:00
|
|
|
|
|
|
|
// MARK: - Read Receipts
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleReadReceipt(_ db: Database, message: ReadReceipt) throws {
|
|
|
|
guard let sender: String = message.sender else { return }
|
|
|
|
guard let timestampMsValues: [Double] = message.timestamps?.map({ Double($0) }) else { return }
|
|
|
|
guard let readTimestampMs: Double = message.receivedTimestamp.map({ Double($0) }) else { return }
|
|
|
|
|
|
|
|
try Interaction.markAsRead(
|
|
|
|
db,
|
|
|
|
recipientId: sender,
|
|
|
|
timestampMsValues: timestampMsValues,
|
|
|
|
readTimestampMs: readTimestampMs
|
|
|
|
)
|
2020-11-25 06:15:16 +01:00
|
|
|
}
|
2020-11-18 05:53:45 +01:00
|
|
|
|
2021-02-25 04:06:40 +01:00
|
|
|
// MARK: - Typing Indicators
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleTypingIndicator(_ db: Database, message: TypingIndicator) throws {
|
|
|
|
switch message.kind {
|
|
|
|
case .started: try showTypingIndicatorIfNeeded(db, for: message.sender)
|
|
|
|
case .stopped: try hideTypingIndicatorIfNeeded(db, for: message.sender)
|
|
|
|
|
|
|
|
default:
|
|
|
|
SNLog("Unknown TypingIndicator Kind ignored")
|
|
|
|
return
|
2020-11-25 06:15:16 +01:00
|
|
|
}
|
|
|
|
}
|
2020-11-18 05:53:45 +01:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func showTypingIndicatorIfNeeded(_ db: Database, for senderPublicKey: String?) throws {
|
|
|
|
guard let senderPublicKey: String = senderPublicKey else { return }
|
|
|
|
|
2020-11-18 05:53:45 +01:00
|
|
|
var threadOrNil: TSContactThread?
|
|
|
|
Storage.read { transaction in
|
2021-05-05 02:00:39 +02:00
|
|
|
threadOrNil = TSContactThread.getWithContactSessionID(senderPublicKey, transaction: transaction)
|
2020-11-18 05:53:45 +01:00
|
|
|
}
|
|
|
|
guard let thread = threadOrNil else { return }
|
|
|
|
func showTypingIndicatorsIfNeeded() {
|
|
|
|
SSKEnvironment.shared.typingIndicators.didReceiveTypingStartedMessage(inThread: thread, recipientId: senderPublicKey, deviceId: 1)
|
|
|
|
}
|
|
|
|
if Thread.current.isMainThread {
|
|
|
|
showTypingIndicatorsIfNeeded()
|
|
|
|
} else {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
showTypingIndicatorsIfNeeded()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func hideTypingIndicatorIfNeeded(_ db: Database, for senderPublicKey: String?) throws {
|
|
|
|
guard let senderPublicKey: String = senderPublicKey else { return }
|
|
|
|
|
2020-11-18 05:53:45 +01:00
|
|
|
var threadOrNil: TSContactThread?
|
|
|
|
Storage.read { transaction in
|
2021-05-05 02:00:39 +02:00
|
|
|
threadOrNil = TSContactThread.getWithContactSessionID(senderPublicKey, transaction: transaction)
|
2020-11-18 05:53:45 +01:00
|
|
|
}
|
|
|
|
guard let thread = threadOrNil else { return }
|
|
|
|
func hideTypingIndicatorsIfNeeded() {
|
|
|
|
SSKEnvironment.shared.typingIndicators.didReceiveTypingStoppedMessage(inThread: thread, recipientId: senderPublicKey, deviceId: 1)
|
|
|
|
}
|
|
|
|
if Thread.current.isMainThread {
|
|
|
|
hideTypingIndicatorsIfNeeded()
|
|
|
|
} else {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
hideTypingIndicatorsIfNeeded()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-25 06:15:16 +01:00
|
|
|
public static func cancelTypingIndicatorsIfNeeded(for senderPublicKey: String) {
|
2020-11-18 05:53:45 +01:00
|
|
|
var threadOrNil: TSContactThread?
|
|
|
|
Storage.read { transaction in
|
2021-05-05 02:00:39 +02:00
|
|
|
threadOrNil = TSContactThread.getWithContactSessionID(senderPublicKey, transaction: transaction)
|
2020-11-18 05:53:45 +01:00
|
|
|
}
|
|
|
|
guard let thread = threadOrNil else { return }
|
|
|
|
func cancelTypingIndicatorsIfNeeded() {
|
|
|
|
SSKEnvironment.shared.typingIndicators.didReceiveIncomingMessage(inThread: thread, recipientId: senderPublicKey, deviceId: 1)
|
|
|
|
}
|
|
|
|
if Thread.current.isMainThread {
|
|
|
|
cancelTypingIndicatorsIfNeeded()
|
|
|
|
} else {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
cancelTypingIndicatorsIfNeeded()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-02-25 04:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2021-03-02 04:25:21 +01:00
|
|
|
// MARK: - Data Extraction Notification
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleDataExtractionNotification(_ db: Database, message: DataExtractionNotification) throws {
|
|
|
|
guard
|
|
|
|
let sender: String = message.sender,
|
|
|
|
let messageKind: DataExtractionNotification.Kind = message.kind,
|
|
|
|
let thread: SessionThread = try? SessionThread.fetchOne(db, id: sender),
|
|
|
|
thread.variant == .contact
|
|
|
|
else { return }
|
|
|
|
|
|
|
|
_ = try Interaction(
|
2022-05-08 14:01:39 +02:00
|
|
|
serverHash: message.serverHash,
|
2022-04-21 08:42:35 +02:00
|
|
|
threadId: thread.id,
|
2022-05-08 14:01:39 +02:00
|
|
|
authorId: sender,
|
2022-04-21 08:42:35 +02:00
|
|
|
variant: {
|
|
|
|
switch messageKind {
|
|
|
|
case .screenshot: return .infoScreenshotNotification
|
|
|
|
case .mediaSaved: return .infoMediaSavedNotification
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
).inserted(db)
|
2021-03-02 04:25:21 +01:00
|
|
|
}
|
|
|
|
|
2021-02-25 04:06:40 +01:00
|
|
|
// MARK: - Expiration Timers
|
2020-11-18 05:53:45 +01:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleExpirationTimerUpdate(_ db: Database, message: ExpirationTimerUpdate) throws {
|
|
|
|
// Get the target thread
|
|
|
|
guard
|
2022-05-06 04:44:26 +02:00
|
|
|
let targetId: String = threadInfo(db, message: message, openGroupId: nil)?.id,
|
2022-04-21 08:42:35 +02:00
|
|
|
let sender: String = message.sender,
|
|
|
|
let thread: SessionThread = try? SessionThread.fetchOne(db, id: targetId)
|
|
|
|
else { return }
|
|
|
|
|
|
|
|
// Update the configuration
|
|
|
|
//
|
|
|
|
// Note: Messages which had been sent during the previous configuration will still
|
|
|
|
// use it's settings (so if you enable, send a message and then disable disappearing
|
|
|
|
// message then the message you had sent will still disappear)
|
|
|
|
let config: DisappearingMessagesConfiguration = try thread.disappearingMessagesConfiguration
|
|
|
|
.fetchOne(db)
|
|
|
|
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(thread.id))
|
|
|
|
.with(
|
|
|
|
// If there is no duration then we should disable the expiration timer
|
2022-05-08 14:01:39 +02:00
|
|
|
isEnabled: ((message.duration ?? 0) > 0),
|
2022-04-21 08:42:35 +02:00
|
|
|
durationSeconds: (
|
|
|
|
message.duration.map { TimeInterval($0) } ??
|
|
|
|
DisappearingMessagesConfiguration.defaultDuration
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
// Add an info message for the user
|
|
|
|
_ = try Interaction(
|
2022-05-08 14:01:39 +02:00
|
|
|
serverHash: nil, // Intentionally null so sync messages are seen as duplicates
|
2022-04-21 08:42:35 +02:00
|
|
|
threadId: thread.id,
|
|
|
|
authorId: sender,
|
|
|
|
variant: .infoDisappearingMessagesUpdate,
|
2022-05-03 09:14:56 +02:00
|
|
|
body: config.messageInfoString(
|
2022-04-21 08:42:35 +02:00
|
|
|
with: (sender != getUserHexEncodedPublicKey(db) ?
|
|
|
|
Profile.displayName(db, id: sender) :
|
|
|
|
nil
|
|
|
|
)
|
|
|
|
),
|
|
|
|
timestampMs: Int64(message.sentTimestamp ?? 0) // Default to `0` if not set
|
|
|
|
).inserted(db)
|
2022-05-08 14:01:39 +02:00
|
|
|
|
|
|
|
// Finally save the changes to the DisappearingMessagesConfiguration (If it's a duplicate
|
|
|
|
// then the interaction unique constraint will prevent the code from getting here)
|
|
|
|
try config.save(db)
|
2020-11-18 05:53:45 +01:00
|
|
|
}
|
2021-01-13 06:10:06 +01:00
|
|
|
|
2021-02-25 04:06:40 +01:00
|
|
|
// MARK: - Configuration Messages
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleConfigurationMessage(_ db: Database, _ message: ConfigurationMessage) throws {
|
|
|
|
let userPublicKey = getUserHexEncodedPublicKey(db)
|
|
|
|
|
2021-03-03 23:58:19 +01:00
|
|
|
guard message.sender == userPublicKey else { return }
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2021-03-03 23:58:19 +01:00
|
|
|
SNLog("Configuration message received.")
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2022-05-06 04:44:26 +02:00
|
|
|
// Note: `message.sentTimestamp` is in ms (convert to TimeInterval before converting to
|
|
|
|
// seconds to maintain the accuracy)
|
2022-03-09 01:52:12 +01:00
|
|
|
let isInitialSync: Bool = (!UserDefaults.standard[.hasSyncedInitialConfiguration])
|
2022-04-21 08:42:35 +02:00
|
|
|
let messageSentTimestamp: TimeInterval = TimeInterval((message.sentTimestamp ?? 0) / 1000)
|
|
|
|
let lastConfigTimestamp: TimeInterval = UserDefaults.standard[.lastConfigurationSync]
|
|
|
|
.defaulting(to: Date(timeIntervalSince1970: 0))
|
|
|
|
.timeIntervalSince1970
|
2022-02-17 01:45:59 +01:00
|
|
|
|
2021-02-23 06:01:06 +01:00
|
|
|
// Profile
|
2022-04-21 08:42:35 +02:00
|
|
|
try updateProfileIfNeeded(
|
|
|
|
db,
|
2022-04-06 07:43:26 +02:00
|
|
|
publicKey: userPublicKey,
|
|
|
|
name: message.displayName,
|
2022-05-03 09:14:56 +02:00
|
|
|
profilePictureUrl: message.profilePictureUrl,
|
2022-04-06 07:43:26 +02:00
|
|
|
profileKey: OWSAES256Key(data: message.profileKey),
|
2022-04-21 08:42:35 +02:00
|
|
|
sentTimestamp: messageSentTimestamp
|
2022-04-06 07:43:26 +02:00
|
|
|
)
|
2022-02-17 01:45:59 +01:00
|
|
|
|
2022-03-09 01:52:12 +01:00
|
|
|
if isInitialSync || messageSentTimestamp > lastConfigTimestamp {
|
|
|
|
if isInitialSync {
|
2022-02-17 01:45:59 +01:00
|
|
|
UserDefaults.standard[.hasSyncedInitialConfiguration] = true
|
|
|
|
NotificationCenter.default.post(name: .initialConfigurationMessageReceived, object: nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
UserDefaults.standard[.lastConfigurationSync] = Date(timeIntervalSince1970: messageSentTimestamp)
|
|
|
|
|
2021-02-25 01:32:53 +01:00
|
|
|
// Contacts
|
2022-04-21 08:42:35 +02:00
|
|
|
try message.contacts.forEach { contactInfo in
|
|
|
|
guard let sessionId: String = contactInfo.publicKey else { return }
|
|
|
|
|
|
|
|
let contact: Contact = Contact.fetchOrCreate(db, id: sessionId)
|
|
|
|
let profile: Profile = Profile.fetchOrCreate(db, id: sessionId)
|
2022-04-06 07:43:26 +02:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
try profile
|
2022-04-06 07:43:26 +02:00
|
|
|
.with(
|
|
|
|
name: contactInfo.displayName,
|
2022-05-03 09:14:56 +02:00
|
|
|
profilePictureUrl: .updateIf(contactInfo.profilePictureUrl),
|
2022-04-06 07:43:26 +02:00
|
|
|
profileEncryptionKey: .updateIf(
|
|
|
|
contactInfo.profileKey.map { OWSAES256Key(data: $0) }
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.save(db)
|
2022-02-21 04:48:53 +01:00
|
|
|
|
2022-05-06 04:44:26 +02:00
|
|
|
/// 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`
|
2022-04-21 08:42:35 +02:00
|
|
|
try contact
|
2022-04-06 07:43:26 +02:00
|
|
|
.with(
|
|
|
|
isApproved: (contactInfo.hasIsApproved && contactInfo.isApproved ?
|
2022-05-06 04:44:26 +02:00
|
|
|
true :
|
|
|
|
.existing
|
2022-04-06 07:43:26 +02:00
|
|
|
),
|
2022-05-06 04:44:26 +02:00
|
|
|
isBlocked: (contactInfo.hasIsBlocked ?
|
|
|
|
.update(contactInfo.isBlocked) :
|
|
|
|
.existing
|
2022-04-06 07:43:26 +02:00
|
|
|
),
|
|
|
|
didApproveMe: (contactInfo.hasDidApproveMe && contactInfo.didApproveMe ?
|
2022-05-06 04:44:26 +02:00
|
|
|
true :
|
|
|
|
.existing
|
2022-04-06 07:43:26 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
.save(db)
|
2022-02-23 07:12:57 +01:00
|
|
|
|
|
|
|
// 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
|
2022-04-06 07:43:26 +02:00
|
|
|
contactInfo.isBlocked != contact.isBlocked,
|
2022-04-21 08:42:35 +02:00
|
|
|
let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId),
|
|
|
|
thread.isMessageRequest(db)
|
2022-02-23 07:12:57 +01:00
|
|
|
{
|
2022-05-06 04:44:26 +02:00
|
|
|
_ = try thread.delete(db)
|
2022-02-23 07:12:57 +01:00
|
|
|
}
|
|
|
|
}
|
2022-02-17 06:29:14 +01:00
|
|
|
}
|
|
|
|
|
2021-02-23 06:01:06 +01:00
|
|
|
// Closed groups
|
2022-03-09 01:52:12 +01:00
|
|
|
//
|
|
|
|
// 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 {
|
2022-04-21 08:42:35 +02:00
|
|
|
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 }
|
|
|
|
|
2022-04-06 07:43:26 +02:00
|
|
|
let keyPair: Box.KeyPair = Box.KeyPair(
|
2022-05-03 09:14:56 +02:00
|
|
|
publicKey: closedGroup.encryptionKeyPublicKey.bytes,
|
|
|
|
secretKey: closedGroup.encryptionKeySecretKey.bytes
|
2022-04-06 07:43:26 +02:00
|
|
|
)
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
try handleNewClosedGroup(
|
|
|
|
db,
|
|
|
|
groupPublicKey: closedGroup.publicKey,
|
|
|
|
name: closedGroup.name,
|
|
|
|
encryptionKeyPair: keyPair,
|
|
|
|
members: [String](closedGroup.members),
|
|
|
|
admins: [String](closedGroup.admins),
|
|
|
|
expirationTimer: closedGroup.expirationTimer,
|
|
|
|
messageSentTimestamp: message.sentTimestamp!
|
|
|
|
)
|
2022-03-09 01:52:12 +01:00
|
|
|
}
|
2021-02-23 06:01:06 +01:00
|
|
|
}
|
2022-03-09 01:52:12 +01:00
|
|
|
|
2021-02-23 06:01:06 +01:00
|
|
|
// Open groups
|
|
|
|
for openGroupURL in message.openGroups {
|
2021-03-29 02:49:59 +02:00
|
|
|
if let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: openGroupURL) {
|
2022-04-21 08:42:35 +02:00
|
|
|
OpenGroupManagerV2.shared
|
|
|
|
.add(db, room: room, server: server, publicKey: publicKey)
|
|
|
|
.retainUntilComplete()
|
2021-03-29 02:49:59 +02:00
|
|
|
}
|
2021-02-23 06:01:06 +01:00
|
|
|
}
|
2021-01-13 06:10:06 +01:00
|
|
|
}
|
|
|
|
}
|
2021-02-25 04:06:40 +01:00
|
|
|
|
2021-07-30 06:21:43 +02:00
|
|
|
// MARK: - Unsend Requests
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
public static func handleUnsendRequest(_ db: Database, message: UnsendRequest) throws {
|
|
|
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
|
|
|
|
2021-10-27 04:51:19 +02:00
|
|
|
guard message.sender == message.author || userPublicKey == message.sender else { return }
|
2022-04-21 08:42:35 +02:00
|
|
|
guard let author: String = message.author, let timestampMs: UInt64 = message.timestamp else { return }
|
|
|
|
|
|
|
|
let maybeInteraction: Interaction? = try Interaction
|
|
|
|
.filter(Interaction.Columns.timestampMs == Int64(timestampMs))
|
|
|
|
.filter(Interaction.Columns.authorId == author)
|
|
|
|
.fetchOne(db)
|
|
|
|
|
2022-05-06 04:44:26 +02:00
|
|
|
guard
|
|
|
|
let interactionId: Int64 = maybeInteraction?.id,
|
|
|
|
let interaction: Interaction = maybeInteraction
|
|
|
|
else { return }
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
// Mark incoming messages as read and remove any of their notifications
|
|
|
|
if interaction.variant == .standardIncoming {
|
2022-05-06 04:44:26 +02:00
|
|
|
try Interaction.markAsRead(
|
|
|
|
db,
|
|
|
|
interactionId: interactionId,
|
|
|
|
threadId: interaction.threadId,
|
|
|
|
includingOlder: false,
|
|
|
|
trySendReadReceipt: false
|
|
|
|
)
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: interaction.notificationIdentifiers)
|
|
|
|
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: interaction.notificationIdentifiers)
|
|
|
|
}
|
|
|
|
|
|
|
|
if author == message.sender {
|
|
|
|
if let serverHash: String = interaction.serverHash {
|
|
|
|
SnodeAPI.deleteMessage(publicKey: author, serverHashes: [serverHash]).retainUntilComplete()
|
2021-08-02 06:03:46 +02:00
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
_ = try interaction
|
|
|
|
.markingAsDeleted()
|
|
|
|
.saved(db)
|
|
|
|
|
|
|
|
_ = try interaction.attachments
|
|
|
|
.deleteAll(db)
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
_ = try interaction.delete(db)
|
2021-07-30 06:21:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-25 04:06:40 +01:00
|
|
|
// MARK: - Visible Messages
|
2020-11-18 05:53:45 +01:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
@discardableResult public static func handleVisibleMessage(
|
|
|
|
_ db: Database,
|
|
|
|
message: VisibleMessage,
|
|
|
|
associatedWithProto proto: SNProtoContent,
|
|
|
|
openGroupId: String?,
|
|
|
|
isBackgroundPoll: Bool
|
|
|
|
) throws -> Int64 {
|
|
|
|
guard let sender: String = message.sender, let dataMessage = proto.dataMessage else {
|
|
|
|
throw MessageReceiverError.invalidMessage
|
|
|
|
}
|
|
|
|
|
2022-05-06 04:44:26 +02:00
|
|
|
// Note: `message.sentTimestamp` is in ms (convert to TimeInterval before converting to
|
|
|
|
// seconds to maintain the accuracy)
|
|
|
|
let messageSentTimestamp: TimeInterval = (TimeInterval(message.sentTimestamp ?? 0) / 1000)
|
2022-04-27 02:48:54 +02:00
|
|
|
let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false)
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2022-05-06 10:07:57 +02:00
|
|
|
// Update profile if needed (want to do this regarless of whether the message exists or
|
|
|
|
// not to ensure the profile info gets sync between a users devices at every chance)
|
2021-05-04 07:46:48 +02:00
|
|
|
if let profile = message.profile {
|
2021-09-30 01:20:20 +02:00
|
|
|
var contactProfileKey: OWSAES256Key? = nil
|
|
|
|
if let profileKey = profile.profileKey { contactProfileKey = OWSAES256Key(data: profileKey) }
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
try updateProfileIfNeeded(
|
|
|
|
db,
|
|
|
|
publicKey: sender,
|
|
|
|
name: profile.displayName,
|
2022-05-03 09:14:56 +02:00
|
|
|
profilePictureUrl: profile.profilePictureUrl,
|
2022-04-21 08:42:35 +02:00
|
|
|
profileKey: contactProfileKey,
|
|
|
|
sentTimestamp: messageSentTimestamp
|
|
|
|
)
|
2020-11-25 06:15:16 +01:00
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2020-11-27 06:22:15 +01:00
|
|
|
// Get or create thread
|
2022-05-06 04:44:26 +02:00
|
|
|
guard let threadInfo: (id: String, variant: SessionThread.Variant) = threadInfo(db, message: message, openGroupId: openGroupId) else {
|
2022-04-21 08:42:35 +02:00
|
|
|
throw MessageReceiverError.noThread
|
2020-11-28 01:48:08 +01:00
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2022-05-06 04:44:26 +02:00
|
|
|
let thread: SessionThread = try SessionThread
|
|
|
|
.fetchOrCreate(db, id: threadInfo.id, variant: threadInfo.variant)
|
|
|
|
|
|
|
|
// Store the message variant so we can run variant-specific behaviours
|
|
|
|
let variant: Interaction.Variant = {
|
|
|
|
if sender == getUserHexEncodedPublicKey(db) {
|
|
|
|
return .standardOutgoing
|
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2022-05-06 04:44:26 +02:00
|
|
|
return .standardIncoming
|
|
|
|
}()
|
|
|
|
|
|
|
|
// Retrieve the disappearing messages config to set the 'expiresInSeconds' value
|
|
|
|
// accoring to the config
|
|
|
|
let disappearingMessagesConfiguration: DisappearingMessagesConfiguration = (try? thread.disappearingMessagesConfiguration.fetchOne(db))
|
|
|
|
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(thread.id))
|
|
|
|
|
|
|
|
// Try to insert the interaction
|
|
|
|
//
|
|
|
|
// Note: There are now a number of unique constraints on the database which
|
|
|
|
// 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
|
2022-05-06 10:07:57 +02:00
|
|
|
let interaction: Interaction
|
2022-05-06 04:44:26 +02:00
|
|
|
|
2022-05-06 10:07:57 +02:00
|
|
|
do {
|
|
|
|
interaction = try Interaction(
|
|
|
|
serverHash: message.serverHash, // Keep track of server hash
|
2022-05-06 04:44:26 +02:00
|
|
|
threadId: thread.id,
|
2022-05-06 10:07:57 +02:00
|
|
|
authorId: sender,
|
|
|
|
variant: variant,
|
|
|
|
body: message.text,
|
|
|
|
timestampMs: Int64(messageSentTimestamp * 1000),
|
|
|
|
// Note: Ensure we don't ever expire open group messages
|
|
|
|
expiresInSeconds: (disappearingMessagesConfiguration.isEnabled && message.openGroupServerMessageId == nil ?
|
|
|
|
disappearingMessagesConfiguration.durationSeconds :
|
|
|
|
nil
|
|
|
|
),
|
|
|
|
expiresStartedAtMs: nil,
|
|
|
|
// 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
|
|
|
|
openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) },
|
|
|
|
openGroupWhisperMods: false,
|
|
|
|
openGroupWhisperTo: nil
|
|
|
|
).inserted(db)
|
|
|
|
}
|
|
|
|
catch {
|
|
|
|
switch error {
|
|
|
|
case DatabaseError.SQLITE_CONSTRAINT_UNIQUE:
|
|
|
|
guard
|
|
|
|
variant == .standardOutgoing,
|
|
|
|
let existingInteractionId: Int64 = try? thread.interactions
|
|
|
|
.select(.id)
|
|
|
|
.filter(Interaction.Columns.timestampMs == (messageSentTimestamp * 1000))
|
|
|
|
.filter(Interaction.Columns.variant == variant)
|
|
|
|
.filter(Interaction.Columns.authorId == sender)
|
|
|
|
.asRequest(of: Int64.self)
|
|
|
|
.fetchOne(db)
|
|
|
|
else { break }
|
|
|
|
|
|
|
|
// If we receive an outgoing message that already exists in the database
|
|
|
|
// then we still need up update the recipient and read states for the
|
|
|
|
// message (even if we don't need to do anything else)
|
|
|
|
try updateRecipientAndReadStates(
|
|
|
|
db,
|
|
|
|
thread: thread,
|
|
|
|
interactionId: existingInteractionId,
|
|
|
|
variant: variant,
|
|
|
|
syncTarget: message.syncTarget
|
|
|
|
)
|
|
|
|
|
|
|
|
default: break
|
|
|
|
}
|
|
|
|
|
|
|
|
throw error
|
2022-04-21 08:42:35 +02:00
|
|
|
}
|
|
|
|
|
2022-05-06 10:07:57 +02:00
|
|
|
guard let interactionId: Int64 = interaction.id else { throw GRDBStorageError.failedToSave }
|
|
|
|
|
|
|
|
// Update and recipient and read states as needed
|
|
|
|
try updateRecipientAndReadStates(
|
|
|
|
db,
|
|
|
|
thread: thread,
|
|
|
|
interactionId: interactionId,
|
|
|
|
variant: variant,
|
|
|
|
syncTarget: message.syncTarget
|
|
|
|
)
|
2022-05-06 04:44:26 +02:00
|
|
|
|
|
|
|
// Parse & persist attachments
|
|
|
|
let attachments: [Attachment] = dataMessage.attachments
|
|
|
|
.compactMap { proto in
|
|
|
|
let attachment: Attachment = Attachment(proto: proto)
|
|
|
|
|
|
|
|
// Attachments on received messages must have a 'downloadUrl' otherwise
|
|
|
|
// they are invalid and we can ignore them
|
|
|
|
return (attachment.downloadUrl != nil ? attachment : nil)
|
|
|
|
}
|
|
|
|
try attachments.saveAll(db)
|
|
|
|
|
|
|
|
message.attachmentIds = attachments.map { $0.id }
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
// Persist quote if needed
|
|
|
|
let quote: Quote? = try? Quote(
|
|
|
|
db,
|
|
|
|
proto: dataMessage,
|
|
|
|
interactionId: interactionId,
|
|
|
|
thread: thread
|
|
|
|
)?.inserted(db)
|
|
|
|
|
|
|
|
// Parse link preview if needed
|
|
|
|
let linkPreview: LinkPreview? = try? LinkPreview(
|
|
|
|
db,
|
|
|
|
proto: dataMessage,
|
|
|
|
body: message.text
|
|
|
|
)?.saved(db)
|
|
|
|
|
|
|
|
// Open group invitations are stored as LinkPreview values so create one if needed
|
|
|
|
if
|
|
|
|
let openGroupInvitationUrl: String = message.openGroupInvitation?.url,
|
|
|
|
let openGroupInvitationName: String = message.openGroupInvitation?.name
|
|
|
|
{
|
|
|
|
try LinkPreview(
|
|
|
|
url: openGroupInvitationUrl,
|
|
|
|
timestamp: LinkPreview.timestampFor(sentTimestampMs: (messageSentTimestamp * 1000)),
|
|
|
|
variant: .openGroupInvitation,
|
|
|
|
title: openGroupInvitationName
|
|
|
|
).save(db)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start attachment downloads if needed (ie. trusted contact or group thread)
|
|
|
|
let isContactTrusted: Bool = ((try? Contact.fetchOne(db, id: sender))?.isTrusted ?? false)
|
|
|
|
|
|
|
|
if isContactTrusted || thread.variant != .contact {
|
|
|
|
attachments
|
|
|
|
.map { $0.id }
|
|
|
|
.appending(quote?.attachmentId)
|
|
|
|
.appending(linkPreview?.attachmentId)
|
|
|
|
.forEach { attachmentId in
|
|
|
|
JobRunner.add(
|
|
|
|
db,
|
|
|
|
job: Job(
|
|
|
|
variant: .attachmentDownload,
|
2022-04-22 10:47:11 +02:00
|
|
|
threadId: thread.id,
|
|
|
|
interactionId: interactionId,
|
2022-04-21 08:42:35 +02:00
|
|
|
details: AttachmentDownloadJob.Details(
|
|
|
|
attachmentId: attachmentId
|
|
|
|
)
|
|
|
|
),
|
|
|
|
canStartJob: isMainAppActive
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cancel any typing indicators if needed
|
|
|
|
if isMainAppActive {
|
|
|
|
cancelTypingIndicatorsIfNeeded(for: message.sender!)
|
2021-09-10 05:48:07 +02:00
|
|
|
}
|
2022-02-02 06:59:56 +01:00
|
|
|
|
|
|
|
// Update the contact's approval status of the current user if needed (if we are getting messages from
|
|
|
|
// them outside of a group then we can assume they have approved the current user)
|
|
|
|
//
|
|
|
|
// Note: This is to resolve a rare edge-case where a conversation was started with a user on an old
|
|
|
|
// version of the app and their message request approval state was set via a migration rather than
|
|
|
|
// by using the approval process
|
2022-04-21 08:42:35 +02:00
|
|
|
if thread.variant == .contact {
|
|
|
|
try updateContactApprovalStatusIfNeeded(
|
2022-04-06 07:43:26 +02:00
|
|
|
db,
|
2022-04-21 08:42:35 +02:00
|
|
|
senderSessionId: sender,
|
|
|
|
threadId: thread.id,
|
2022-04-06 07:43:26 +02:00
|
|
|
forceConfigSync: false
|
2022-02-02 06:59:56 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2020-11-25 06:15:16 +01:00
|
|
|
// Notify the user if needed
|
2022-05-06 04:44:26 +02:00
|
|
|
guard variant == .standardIncoming else { return interactionId }
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2021-11-23 01:05:04 +01:00
|
|
|
// Use the same identifier for notifications when in backgroud polling to prevent spam
|
2022-04-21 08:42:35 +02:00
|
|
|
SSKEnvironment.shared.notificationsManager.wrappedValue?
|
|
|
|
.notifyUser(
|
|
|
|
db,
|
|
|
|
for: interaction,
|
|
|
|
in: thread,
|
|
|
|
isBackgroundPoll: isBackgroundPoll
|
|
|
|
)
|
|
|
|
|
|
|
|
return interactionId
|
2020-11-25 06:15:16 +01:00
|
|
|
}
|
2021-03-04 04:40:58 +01:00
|
|
|
|
2022-05-06 10:07:57 +02:00
|
|
|
private static func updateRecipientAndReadStates(
|
|
|
|
_ db: Database,
|
|
|
|
thread: SessionThread,
|
|
|
|
interactionId: Int64,
|
|
|
|
variant: Interaction.Variant,
|
|
|
|
syncTarget: String?
|
|
|
|
) throws {
|
|
|
|
guard variant == .standardOutgoing else { return }
|
|
|
|
|
|
|
|
if let syncTarget: String = syncTarget {
|
|
|
|
try RecipientState(
|
|
|
|
interactionId: interactionId,
|
|
|
|
recipientId: syncTarget,
|
|
|
|
state: .sent
|
|
|
|
).save(db)
|
|
|
|
}
|
|
|
|
else if thread.variant == .closedGroup {
|
|
|
|
try GroupMember
|
|
|
|
.filter(GroupMember.Columns.groupId == thread.id)
|
|
|
|
.fetchAll(db)
|
|
|
|
.forEach { member in
|
|
|
|
try RecipientState(
|
|
|
|
interactionId: interactionId,
|
|
|
|
recipientId: member.profileId,
|
|
|
|
state: .sent
|
|
|
|
).save(db)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// For outgoing messages mark it and all older interactions as read
|
|
|
|
try Interaction.markAsRead(
|
|
|
|
db,
|
|
|
|
interactionId: interactionId,
|
|
|
|
threadId: thread.id,
|
|
|
|
includingOlder: true,
|
|
|
|
trySendReadReceipt: true
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-03-04 04:40:58 +01:00
|
|
|
// MARK: - Profile Updating
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
private static func updateProfileIfNeeded(
|
2022-05-06 10:07:57 +02:00
|
|
|
_ db: Database,
|
|
|
|
publicKey: String,
|
2022-04-21 08:42:35 +02:00
|
|
|
name: String?,
|
|
|
|
profilePictureUrl: String?,
|
|
|
|
profileKey: OWSAES256Key?,
|
|
|
|
sentTimestamp: TimeInterval
|
|
|
|
) throws {
|
|
|
|
let isCurrentUser = (publicKey == getUserHexEncodedPublicKey(db))
|
2022-04-06 07:43:26 +02:00
|
|
|
var profile: Profile = Profile.fetchOrCreate(id: publicKey)
|
|
|
|
|
2021-03-04 04:40:58 +01:00
|
|
|
// Name
|
2022-04-06 07:43:26 +02:00
|
|
|
if let name = name, name != profile.name {
|
2021-03-04 04:40:58 +01:00
|
|
|
let shouldUpdate: Bool
|
|
|
|
if isCurrentUser {
|
2022-04-21 08:42:35 +02:00
|
|
|
shouldUpdate = given(UserDefaults.standard[.lastDisplayNameUpdate]) {
|
|
|
|
sentTimestamp > $0.timeIntervalSince1970
|
|
|
|
}
|
|
|
|
.defaulting(to: true)
|
|
|
|
}
|
|
|
|
else {
|
2021-03-04 04:40:58 +01:00
|
|
|
shouldUpdate = true
|
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2021-03-04 04:40:58 +01:00
|
|
|
if shouldUpdate {
|
|
|
|
if isCurrentUser {
|
2022-04-21 08:42:35 +02:00
|
|
|
UserDefaults.standard[.lastDisplayNameUpdate] = Date(timeIntervalSince1970: sentTimestamp)
|
2021-03-04 04:40:58 +01:00
|
|
|
}
|
2022-04-06 07:43:26 +02:00
|
|
|
|
|
|
|
profile = profile.with(name: name)
|
2021-03-04 04:40:58 +01:00
|
|
|
}
|
|
|
|
}
|
2022-04-06 07:43:26 +02:00
|
|
|
|
2021-03-04 04:40:58 +01:00
|
|
|
// Profile picture & profile key
|
2022-04-06 07:43:26 +02:00
|
|
|
if
|
2022-04-21 08:42:35 +02:00
|
|
|
let profileKey: OWSAES256Key = profileKey,
|
|
|
|
let profilePictureUrl: String = profilePictureUrl,
|
2022-04-06 07:43:26 +02:00
|
|
|
profileKey.keyData.count == kAES256_KeyByteLength,
|
|
|
|
profileKey != profile.profileEncryptionKey
|
|
|
|
{
|
2021-03-04 04:40:58 +01:00
|
|
|
let shouldUpdate: Bool
|
|
|
|
if isCurrentUser {
|
2022-04-21 08:42:35 +02:00
|
|
|
shouldUpdate = given(UserDefaults.standard[.lastProfilePictureUpdate]) {
|
|
|
|
sentTimestamp > $0.timeIntervalSince1970
|
|
|
|
}
|
|
|
|
.defaulting(to: true)
|
|
|
|
}
|
|
|
|
else {
|
2021-03-04 04:40:58 +01:00
|
|
|
shouldUpdate = true
|
|
|
|
}
|
2022-04-06 07:43:26 +02:00
|
|
|
|
2021-03-04 04:40:58 +01:00
|
|
|
if shouldUpdate {
|
|
|
|
if isCurrentUser {
|
2022-04-21 08:42:35 +02:00
|
|
|
UserDefaults.standard[.lastProfilePictureUpdate] = Date(timeIntervalSince1970: sentTimestamp)
|
2021-03-04 04:40:58 +01:00
|
|
|
}
|
2022-04-06 07:43:26 +02:00
|
|
|
|
|
|
|
profile = profile.with(
|
2022-04-21 08:42:35 +02:00
|
|
|
profilePictureUrl: .update(profilePictureUrl),
|
2022-04-06 07:43:26 +02:00
|
|
|
profileEncryptionKey: .update(profileKey)
|
|
|
|
)
|
2021-03-04 04:40:58 +01:00
|
|
|
}
|
|
|
|
}
|
2022-04-06 07:43:26 +02:00
|
|
|
|
2021-03-04 04:40:58 +01:00
|
|
|
// Persist changes
|
2022-04-21 08:42:35 +02:00
|
|
|
try profile.save(db)
|
2022-04-06 07:43:26 +02:00
|
|
|
|
2021-07-23 01:00:28 +02:00
|
|
|
// Download the profile picture if needed
|
2022-04-21 08:42:35 +02:00
|
|
|
db.afterNextTransactionCommit { _ in
|
2022-04-06 07:43:26 +02:00
|
|
|
ProfileManager.downloadAvatar(for: profile)
|
2021-07-23 01:00:28 +02:00
|
|
|
}
|
2021-03-04 04:40:58 +01:00
|
|
|
}
|
2021-02-25 04:06:40 +01:00
|
|
|
|
|
|
|
// MARK: - Closed Groups
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
public static func handleClosedGroupControlMessage(_ db: Database, _ message: ClosedGroupControlMessage) throws {
|
2021-01-04 05:30:13 +01:00
|
|
|
switch message.kind! {
|
2022-04-21 08:42:35 +02:00
|
|
|
case .new: try handleNewClosedGroup(db, message: message)
|
|
|
|
case .encryptionKeyPair: try handleClosedGroupEncryptionKeyPair(db, message: message)
|
|
|
|
case .nameChange: try handleClosedGroupNameChanged(db, message: message)
|
|
|
|
case .membersAdded: try handleClosedGroupMembersAdded(db, message: message)
|
|
|
|
case .membersRemoved: try handleClosedGroupMembersRemoved(db, message: message)
|
|
|
|
case .memberLeft: try handleClosedGroupMemberLeft(db, message: message)
|
|
|
|
case .encryptionKeyPairRequest:
|
|
|
|
handleClosedGroupEncryptionKeyPairRequest(db, message: message) // Currently not used
|
2021-01-04 05:30:13 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleNewClosedGroup(_ db: Database, message: ClosedGroupControlMessage) throws {
|
|
|
|
guard case let .new(publicKeyAsData, name, encryptionKeyPair, membersAsData, adminsAsData, expirationTimer) = message.kind else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
guard let sentTimestamp: UInt64 = message.sentTimestamp else { return }
|
|
|
|
|
|
|
|
try handleNewClosedGroup(
|
|
|
|
db,
|
|
|
|
groupPublicKey: publicKeyAsData.toHexString(),
|
|
|
|
name: name,
|
|
|
|
encryptionKeyPair: encryptionKeyPair,
|
|
|
|
members: membersAsData.map { $0.toHexString() },
|
|
|
|
admins: adminsAsData.map { $0.toHexString() },
|
|
|
|
expirationTimer: expirationTimer,
|
|
|
|
messageSentTimestamp: sentTimestamp
|
|
|
|
)
|
2021-01-13 03:38:07 +01:00
|
|
|
}
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleNewClosedGroup(
|
|
|
|
_ db: Database,
|
|
|
|
groupPublicKey: String,
|
|
|
|
name: String,
|
|
|
|
encryptionKeyPair: Box.KeyPair,
|
|
|
|
members: [String],
|
|
|
|
admins: [String],
|
|
|
|
expirationTimer: UInt32,
|
|
|
|
messageSentTimestamp: UInt64
|
|
|
|
) throws {
|
2022-02-02 06:59:56 +01:00
|
|
|
// With new closed groups we only want to create them if the admin creating the closed group is an
|
|
|
|
// approved contact (to prevent spam via closed groups getting around message requests if users are
|
|
|
|
// on old or modified clients)
|
|
|
|
var hasApprovedAdmin: Bool = false
|
|
|
|
|
|
|
|
for adminId in admins {
|
2022-04-06 07:43:26 +02:00
|
|
|
if let contact: Contact = try? Contact.fetchOne(db, id: adminId), contact.isApproved {
|
2022-02-02 06:59:56 +01:00
|
|
|
hasApprovedAdmin = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
guard hasApprovedAdmin else { return }
|
|
|
|
|
2021-01-04 05:30:13 +01:00
|
|
|
// Create the group
|
2022-04-21 08:42:35 +02:00
|
|
|
let groupAlreadyExisted: Bool = ((try? SessionThread.exists(db, id: groupPublicKey)) ?? false)
|
|
|
|
let thread: SessionThread = try SessionThread
|
|
|
|
.fetchOrCreate(db, id: groupPublicKey, variant: .closedGroup)
|
2022-05-08 14:01:39 +02:00
|
|
|
.with(shouldBeVisible: true)
|
|
|
|
.saved(db)
|
2022-04-21 08:42:35 +02:00
|
|
|
let closedGroup: ClosedGroup = try ClosedGroup(
|
|
|
|
threadId: groupPublicKey,
|
|
|
|
name: name,
|
2022-05-08 14:01:39 +02:00
|
|
|
formationTimestamp: (TimeInterval(messageSentTimestamp) / 1000)
|
2022-04-21 08:42:35 +02:00
|
|
|
).saved(db)
|
|
|
|
|
|
|
|
// Clear the zombie list if the group wasn't active (ie. had no keys)
|
|
|
|
if ((try? closedGroup.keyPairs.fetchCount(db)) ?? 0) == 0 {
|
|
|
|
try closedGroup.zombies.deleteAll(db)
|
2022-02-02 06:59:56 +01:00
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
// Notify the user
|
|
|
|
if !groupAlreadyExisted {
|
|
|
|
// Note: We don't provide a `serverHash` in this case as we want to allow duplicates
|
|
|
|
// to avoid the following situation:
|
|
|
|
// • The app performed a background poll or received a push notification
|
|
|
|
// • This method was invoked and the received message timestamps table was updated
|
|
|
|
// • Processing wasn't finished
|
|
|
|
// • The user doesn't see the new closed group
|
|
|
|
_ = try Interaction(
|
|
|
|
threadId: thread.id,
|
|
|
|
authorId: getUserHexEncodedPublicKey(db),
|
|
|
|
variant: .infoClosedGroupCreated,
|
2022-05-08 14:01:39 +02:00
|
|
|
timestampMs: Int64(messageSentTimestamp)
|
2022-04-21 08:42:35 +02:00
|
|
|
).inserted(db)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the DisappearingMessages config
|
|
|
|
try thread.disappearingMessagesConfiguration
|
|
|
|
.fetchOne(db)
|
|
|
|
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(thread.id))
|
|
|
|
.with(
|
|
|
|
isEnabled: (expirationTimer > 0),
|
|
|
|
durationSeconds: TimeInterval(expirationTimer > 0 ?
|
|
|
|
expirationTimer :
|
|
|
|
(24 * 60 * 60)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.save(db)
|
2022-02-02 06:59:56 +01:00
|
|
|
|
2021-01-08 00:32:54 +01:00
|
|
|
// Store the key pair
|
2022-04-21 08:42:35 +02:00
|
|
|
try ClosedGroupKeyPair(
|
2022-04-22 10:47:11 +02:00
|
|
|
threadId: groupPublicKey,
|
|
|
|
publicKey: Data(encryptionKeyPair.publicKey),
|
2022-04-21 08:42:35 +02:00
|
|
|
secretKey: Data(encryptionKeyPair.secretKey),
|
|
|
|
receivedTimestamp: Date().timeIntervalSince1970
|
|
|
|
).insert(db)
|
|
|
|
|
2021-05-12 02:33:29 +02:00
|
|
|
// Start polling
|
|
|
|
ClosedGroupPoller.shared.startPolling(for: groupPublicKey)
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2021-01-04 05:30:13 +01:00
|
|
|
// Notify the PN server
|
|
|
|
let _ = PushNotificationAPI.performOperation(.subscribe, for: groupPublicKey, publicKey: getUserHexEncodedPublicKey())
|
|
|
|
}
|
2021-01-22 01:02:19 +01:00
|
|
|
|
2021-04-16 01:40:54 +02:00
|
|
|
/// Extracts and adds the new encryption key pair to our list of key pairs if there is one for our public key, AND the message was
|
|
|
|
/// sent by the group admin.
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleClosedGroupEncryptionKeyPair(_ db: Database, message: ClosedGroupControlMessage) throws {
|
|
|
|
guard
|
|
|
|
case let .encryptionKeyPair(explicitGroupPublicKey, wrappers) = message.kind,
|
|
|
|
let groupPublicKey: String = (explicitGroupPublicKey?.toHexString() ?? message.groupPublicKey)
|
|
|
|
else { return }
|
|
|
|
guard let userKeyPair: Box.KeyPair = Identity.fetchUserKeyPair(db) else {
|
2021-01-22 01:02:19 +01:00
|
|
|
return SNLog("Couldn't find user X25519 key pair.")
|
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: groupPublicKey) else {
|
2021-01-22 01:02:19 +01:00
|
|
|
return SNLog("Ignoring closed group encryption key pair for nonexistent group.")
|
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
guard let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(db) else { return }
|
|
|
|
guard let groupAdmins: [GroupMember] = try? closedGroup.admins.fetchAll(db) else { return }
|
|
|
|
guard let sender: String = message.sender, groupAdmins.contains(where: { $0.profileId == sender }) else {
|
2021-04-15 05:15:15 +02:00
|
|
|
return SNLog("Ignoring closed group encryption key pair from non-admin.")
|
2021-01-22 01:02:19 +01:00
|
|
|
}
|
|
|
|
// Find our wrapper and decrypt it if possible
|
2022-04-21 08:42:35 +02:00
|
|
|
let userPublicKey: String = userKeyPair.publicKey.toHexString()
|
|
|
|
|
|
|
|
guard
|
|
|
|
let wrapper = wrappers.first(where: { $0.publicKey == userPublicKey }),
|
|
|
|
let encryptedKeyPair = wrapper.encryptedKeyPair
|
|
|
|
else { return }
|
|
|
|
|
2021-01-22 01:02:19 +01:00
|
|
|
let plaintext: Data
|
|
|
|
do {
|
2022-04-21 08:42:35 +02:00
|
|
|
plaintext = try MessageReceiver.decryptWithSessionProtocol(
|
|
|
|
ciphertext: encryptedKeyPair,
|
|
|
|
using: userKeyPair
|
|
|
|
).plaintext
|
|
|
|
}
|
|
|
|
catch {
|
2021-01-22 01:02:19 +01:00
|
|
|
return SNLog("Couldn't decrypt closed group encryption key pair.")
|
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2021-01-22 01:02:19 +01:00
|
|
|
// Parse it
|
2021-01-25 03:50:18 +01:00
|
|
|
let proto: SNProtoKeyPair
|
2021-01-22 01:02:19 +01:00
|
|
|
do {
|
2021-01-25 03:50:18 +01:00
|
|
|
proto = try SNProtoKeyPair.parseData(plaintext)
|
2022-04-21 08:42:35 +02:00
|
|
|
}
|
|
|
|
catch {
|
2021-01-22 01:02:19 +01:00
|
|
|
return SNLog("Couldn't parse closed group encryption key pair.")
|
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2022-04-22 10:47:11 +02:00
|
|
|
do {
|
|
|
|
try ClosedGroupKeyPair(
|
|
|
|
threadId: groupPublicKey,
|
|
|
|
publicKey: proto.publicKey.removing05PrefixIfNeeded(),
|
|
|
|
secretKey: proto.privateKey,
|
|
|
|
receivedTimestamp: Date().timeIntervalSince1970
|
|
|
|
).insert(db)
|
|
|
|
}
|
|
|
|
catch {
|
2021-02-08 01:33:47 +01:00
|
|
|
return SNLog("Ignoring duplicate closed group encryption key pair.")
|
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2021-01-22 01:02:19 +01:00
|
|
|
SNLog("Received a new closed group encryption key pair.")
|
|
|
|
}
|
2021-01-04 05:30:13 +01:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleClosedGroupNameChanged(_ db: Database, message: ClosedGroupControlMessage) throws {
|
2021-01-22 01:02:19 +01:00
|
|
|
guard case let .nameChange(name) = message.kind else { return }
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
try performIfValid(db, message: message) { id, sender, thread, closedGroup in
|
|
|
|
try closedGroup
|
|
|
|
.with(name: name)
|
|
|
|
.save(db)
|
|
|
|
|
2021-01-22 01:02:19 +01:00
|
|
|
// Notify the user if needed
|
2022-04-21 08:42:35 +02:00
|
|
|
guard name != closedGroup.name else { return }
|
|
|
|
|
|
|
|
_ = try Interaction(
|
2022-05-08 14:01:39 +02:00
|
|
|
serverHash: message.serverHash,
|
2022-04-21 08:42:35 +02:00
|
|
|
threadId: thread.id,
|
|
|
|
authorId: sender,
|
|
|
|
variant: .infoClosedGroupUpdated,
|
|
|
|
body: ClosedGroupControlMessage.Kind
|
|
|
|
.nameChange(name: name)
|
|
|
|
.infoMessage(db, sender: sender),
|
|
|
|
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
|
|
|
).inserted(db)
|
2021-01-22 01:02:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleClosedGroupMembersAdded(_ db: Database, message: ClosedGroupControlMessage) throws {
|
2021-01-22 01:07:22 +01:00
|
|
|
guard case let .membersAdded(membersAsData) = message.kind else { return }
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
try performIfValid(db, message: message) { id, sender, thread, closedGroup in
|
|
|
|
guard let groupMembers: [GroupMember] = try? closedGroup.members.fetchAll(db) else { return }
|
|
|
|
guard let groupAdmins: [GroupMember] = try? closedGroup.admins.fetchAll(db) else { return }
|
|
|
|
|
2021-01-22 01:02:19 +01:00
|
|
|
// Update the group
|
2022-04-21 08:42:35 +02:00
|
|
|
let addedMembers: [String] = membersAsData.map { $0.toHexString() }
|
|
|
|
let members: Set<String> = Set(groupMembers.map { $0.profileId }).union(addedMembers)
|
|
|
|
|
|
|
|
try addedMembers.forEach { memberId in
|
|
|
|
try GroupMember(
|
|
|
|
groupId: id,
|
|
|
|
profileId: memberId,
|
|
|
|
role: .standard
|
|
|
|
).save(db)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send the latest encryption key pair to the added members if the current user is
|
|
|
|
// the admin of the group
|
2021-04-26 01:54:06 +02:00
|
|
|
//
|
|
|
|
// This fixes a race condition where:
|
|
|
|
// • A member removes another member.
|
|
|
|
// • A member adds someone to the group and sends them the latest group key pair.
|
|
|
|
// • The admin is offline during all of this.
|
2022-04-21 08:42:35 +02:00
|
|
|
// • When the admin comes back online they see the member removed message and generate +
|
|
|
|
// distribute a new key pair, but they don't know about the added member yet.
|
2021-04-26 01:54:06 +02:00
|
|
|
// • Now they see the member added message.
|
|
|
|
//
|
2022-04-21 08:42:35 +02:00
|
|
|
// Without the code below, the added member(s) would never get the key pair that was
|
|
|
|
// generated by the admin when they saw the member removed message.
|
|
|
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
|
|
|
|
|
|
|
if groupAdmins.contains(where: { $0.profileId == userPublicKey }) {
|
|
|
|
addedMembers.forEach { memberId in
|
|
|
|
MessageSender.sendLatestEncryptionKeyPair(db, to: memberId, for: id)
|
2021-02-11 06:14:03 +01:00
|
|
|
}
|
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2021-06-04 08:23:59 +02:00
|
|
|
// Update zombie members in case the added members are zombies
|
2022-04-21 08:42:35 +02:00
|
|
|
let zombies: [GroupMember] = ((try? closedGroup.zombies.fetchAll(db)) ?? [])
|
|
|
|
|
|
|
|
if !zombies.map { $0.profileId }.asSet().intersection(addedMembers).isEmpty {
|
|
|
|
try zombies
|
|
|
|
.filter { !addedMembers.contains($0.profileId) }
|
|
|
|
.deleteAll(db)
|
2021-06-04 03:50:24 +02:00
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2021-01-22 01:02:19 +01:00
|
|
|
// Notify the user if needed
|
2022-04-21 08:42:35 +02:00
|
|
|
guard members != Set(groupMembers.map { $0.profileId }) else { return }
|
|
|
|
|
|
|
|
_ = try Interaction(
|
2022-05-08 14:01:39 +02:00
|
|
|
serverHash: message.serverHash,
|
2022-04-21 08:42:35 +02:00
|
|
|
threadId: thread.id,
|
|
|
|
authorId: sender,
|
|
|
|
variant: .infoClosedGroupUpdated,
|
|
|
|
body: ClosedGroupControlMessage.Kind
|
|
|
|
.membersAdded(
|
|
|
|
members: addedMembers
|
|
|
|
.asSet()
|
|
|
|
.subtracting(groupMembers.map { $0.profileId })
|
|
|
|
.map { Data(hex: $0) }
|
|
|
|
)
|
|
|
|
.infoMessage(db, sender: sender),
|
|
|
|
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
|
|
|
).inserted(db)
|
2021-01-22 01:02:19 +01:00
|
|
|
}
|
|
|
|
}
|
2021-01-25 03:50:18 +01:00
|
|
|
|
2021-04-16 01:40:54 +02:00
|
|
|
/// Removes the given members from the group IF
|
|
|
|
/// • it wasn't the admin that was removed (that should happen through a `MEMBER_LEFT` message).
|
|
|
|
/// • the admin sent the message (only the admin can truly remove members).
|
|
|
|
/// If we're among the users that were removed, delete all encryption key pairs and the group public key, unsubscribe
|
|
|
|
/// from push notifications for this closed group, and remove the given members from the zombie list for this group.
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleClosedGroupMembersRemoved(_ db: Database, message: ClosedGroupControlMessage) throws {
|
2021-01-22 01:07:22 +01:00
|
|
|
guard case let .membersRemoved(membersAsData) = message.kind else { return }
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
try performIfValid(db, message: message) { id, sender, thread, closedGroup in
|
2021-01-22 03:25:23 +01:00
|
|
|
// Check that the admin wasn't removed
|
2022-04-21 08:42:35 +02:00
|
|
|
guard let groupMembers: [GroupMember] = try? closedGroup.members.fetchAll(db) else { return }
|
|
|
|
guard let groupAdmins: [GroupMember] = try? closedGroup.admins.fetchAll(db) else { return }
|
|
|
|
|
2021-04-16 02:56:10 +02:00
|
|
|
let removedMembers = membersAsData.map { $0.toHexString() }
|
2022-04-21 08:42:35 +02:00
|
|
|
let members = Set(groupMembers.map { $0.profileId }).subtracting(removedMembers)
|
|
|
|
|
|
|
|
guard let firstAdminId: String = groupAdmins.first?.profileId, members.contains(firstAdminId) else {
|
2021-01-22 01:02:19 +01:00
|
|
|
return SNLog("Ignoring invalid closed group update.")
|
|
|
|
}
|
2021-04-15 05:15:15 +02:00
|
|
|
// Check that the message was sent by the group admin
|
2022-04-21 08:42:35 +02:00
|
|
|
guard groupAdmins.contains(where: { $0.profileId == sender }) else {
|
2021-04-15 05:15:15 +02:00
|
|
|
return SNLog("Ignoring invalid closed group update.")
|
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2021-01-22 01:02:19 +01:00
|
|
|
// If the current user was removed:
|
|
|
|
// • Stop polling for the group
|
|
|
|
// • Remove the key pairs associated with the group
|
|
|
|
// • Notify the PN server
|
2022-04-22 10:47:11 +02:00
|
|
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
|
|
|
let wasCurrentUserRemoved: Bool = !members.contains(userPublicKey)
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2021-01-22 01:02:19 +01:00
|
|
|
if wasCurrentUserRemoved {
|
2022-04-21 08:42:35 +02:00
|
|
|
ClosedGroupPoller.shared.stopPolling(for: id)
|
|
|
|
|
|
|
|
_ = try closedGroup
|
|
|
|
.keyPairs
|
|
|
|
.deleteAll(db)
|
2022-05-08 14:01:39 +02:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
let _ = PushNotificationAPI.performOperation(
|
|
|
|
.unsubscribe,
|
|
|
|
for: id,
|
|
|
|
publicKey: userPublicKey
|
|
|
|
)
|
2021-01-22 01:02:19 +01:00
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
// Remove the member from the group and it's zombies
|
|
|
|
try closedGroup.members
|
|
|
|
.filter(removedMembers.contains(GroupMember.Columns.profileId))
|
|
|
|
.deleteAll(db)
|
|
|
|
try closedGroup.zombies
|
|
|
|
.filter(removedMembers.contains(GroupMember.Columns.profileId))
|
|
|
|
.deleteAll(db)
|
|
|
|
|
2021-01-22 01:02:19 +01:00
|
|
|
// Notify the user if needed
|
2022-04-21 08:42:35 +02:00
|
|
|
guard members != Set(groupMembers.map { $0.profileId }) else { return }
|
|
|
|
|
|
|
|
_ = try Interaction(
|
2022-05-08 14:01:39 +02:00
|
|
|
serverHash: message.serverHash,
|
2022-04-21 08:42:35 +02:00
|
|
|
threadId: thread.id,
|
|
|
|
authorId: sender,
|
|
|
|
variant: (wasCurrentUserRemoved ? .infoClosedGroupCurrentUserLeft : .infoClosedGroupUpdated),
|
|
|
|
body: ClosedGroupControlMessage.Kind
|
|
|
|
.membersRemoved(
|
|
|
|
members: removedMembers
|
|
|
|
.asSet()
|
|
|
|
.subtracting(groupMembers.map { $0.profileId })
|
|
|
|
.map { Data(hex: $0) }
|
|
|
|
)
|
|
|
|
.infoMessage(db, sender: sender),
|
|
|
|
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
|
|
|
).inserted(db)
|
2021-01-22 01:02:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-16 01:40:54 +02:00
|
|
|
/// If a regular member left:
|
|
|
|
/// • Mark them as a zombie (to be removed by the admin later).
|
|
|
|
/// If the admin left:
|
|
|
|
/// • Unsubscribe from PNs, delete the group public key, etc. as the group will be disbanded.
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleClosedGroupMemberLeft(_ db: Database, message: ClosedGroupControlMessage) throws {
|
2021-01-22 01:07:22 +01:00
|
|
|
guard case .memberLeft = message.kind else { return }
|
2022-04-06 07:43:26 +02:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
try performIfValid(db, message: message) { id, sender, thread, closedGroup in
|
|
|
|
guard let allGroupMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db) else {
|
|
|
|
return
|
2021-01-22 01:02:19 +01:00
|
|
|
}
|
2022-04-06 07:43:26 +02:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
let didAdminLeave: Bool = allGroupMembers.contains(where: { member in
|
|
|
|
member.role == .admin && member.profileId == sender
|
|
|
|
})
|
|
|
|
let members: [GroupMember] = allGroupMembers.filter { $0.role == .standard }
|
|
|
|
let membersToRemove: [GroupMember] = members
|
|
|
|
.filter { member in
|
|
|
|
didAdminLeave || // If the admin leaves the group is disbanded
|
|
|
|
member.profileId == sender
|
|
|
|
}
|
|
|
|
let updatedMemberIds: Set<String> = members
|
|
|
|
.map { $0.profileId }
|
|
|
|
.asSet()
|
|
|
|
.subtracting(membersToRemove.map { $0.profileId })
|
2022-04-06 07:43:26 +02:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
if didAdminLeave {
|
|
|
|
// Remove the group from the database and unsubscribe from PNs
|
|
|
|
ClosedGroupPoller.shared.stopPolling(for: id)
|
|
|
|
|
|
|
|
_ = try closedGroup
|
|
|
|
.keyPairs
|
|
|
|
.deleteAll(db)
|
2022-05-08 14:01:39 +02:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
let _ = PushNotificationAPI.performOperation(
|
|
|
|
.unsubscribe,
|
|
|
|
for: id,
|
|
|
|
publicKey: getUserHexEncodedPublicKey(db)
|
|
|
|
)
|
2022-04-06 07:43:26 +02:00
|
|
|
}
|
|
|
|
else {
|
2022-04-21 08:42:35 +02:00
|
|
|
try GroupMember(
|
|
|
|
groupId: id,
|
|
|
|
profileId: sender,
|
|
|
|
role: .zombie
|
|
|
|
).save(db)
|
2021-05-05 02:38:09 +02:00
|
|
|
}
|
2022-04-06 07:43:26 +02:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
// Update the group
|
|
|
|
try membersToRemove
|
|
|
|
.deleteAll(db)
|
|
|
|
|
|
|
|
// Notify the user if needed
|
|
|
|
guard updatedMemberIds != Set(members.map { $0.profileId }) else { return }
|
|
|
|
|
|
|
|
_ = try Interaction(
|
2022-05-08 14:01:39 +02:00
|
|
|
serverHash: message.serverHash,
|
2022-04-21 08:42:35 +02:00
|
|
|
threadId: thread.id,
|
|
|
|
authorId: sender,
|
|
|
|
variant: .infoClosedGroupUpdated,
|
|
|
|
body: ClosedGroupControlMessage.Kind
|
|
|
|
.memberLeft
|
|
|
|
.infoMessage(db, sender: sender),
|
|
|
|
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
|
|
|
).inserted(db)
|
2021-01-22 01:02:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleClosedGroupEncryptionKeyPairRequest(_ db: Database, message: ClosedGroupControlMessage) {
|
2021-04-15 05:15:15 +02:00
|
|
|
/*
|
2021-02-08 01:33:47 +01:00
|
|
|
guard case .encryptionKeyPairRequest = message.kind else { return }
|
|
|
|
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
|
|
|
guard let groupPublicKey = message.groupPublicKey else { return }
|
|
|
|
performIfValid(for: message, using: transaction) { groupID, _, group in
|
|
|
|
let publicKey = message.sender!
|
|
|
|
// Guard against self-sends
|
|
|
|
guard publicKey != getUserHexEncodedPublicKey() else {
|
|
|
|
return SNLog("Ignoring invalid closed group update.")
|
|
|
|
}
|
2021-02-11 06:14:03 +01:00
|
|
|
MessageSender.sendLatestEncryptionKeyPair(to: publicKey, for: groupPublicKey, using: transaction)
|
2021-02-08 01:33:47 +01:00
|
|
|
}
|
2021-04-15 05:15:15 +02:00
|
|
|
*/
|
2021-02-08 01:33:47 +01:00
|
|
|
}
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func performIfValid(
|
|
|
|
_ db: Database,
|
|
|
|
message: ClosedGroupControlMessage,
|
|
|
|
_ update: (String, String, SessionThread, ClosedGroup
|
|
|
|
) throws -> Void) throws {
|
|
|
|
guard let groupPublicKey: String = message.groupPublicKey else { return }
|
|
|
|
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: groupPublicKey) else {
|
2021-01-22 01:02:19 +01:00
|
|
|
return SNLog("Ignoring closed group update for nonexistent group.")
|
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
guard let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(db) else { return }
|
|
|
|
|
2021-01-22 01:02:19 +01:00
|
|
|
// Check that the message isn't from before the group was created
|
2022-04-21 08:42:35 +02:00
|
|
|
guard Double(message.sentTimestamp ?? 0) > closedGroup.formationTimestamp else {
|
|
|
|
return SNLog("Ignoring closed group update from before thread was created.")
|
2021-01-22 01:02:19 +01:00
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
guard let sender: String = message.sender else { return }
|
|
|
|
guard let members: [GroupMember] = try? closedGroup.members.fetchAll(db) else { return }
|
|
|
|
|
2021-01-22 01:02:19 +01:00
|
|
|
// Check that the sender is a member of the group
|
2022-04-21 08:42:35 +02:00
|
|
|
guard members.contains(where: { $0.profileId == sender }) else {
|
2021-01-22 01:02:19 +01:00
|
|
|
return SNLog("Ignoring closed group update from non-member.")
|
|
|
|
}
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
try update(groupPublicKey, sender, thread, closedGroup)
|
2021-01-22 01:02:19 +01:00
|
|
|
}
|
2022-02-02 06:59:56 +01:00
|
|
|
|
|
|
|
// MARK: - Message Requests
|
|
|
|
|
2022-02-17 01:40:35 +01:00
|
|
|
private static func updateContactApprovalStatusIfNeeded(
|
2022-04-06 07:43:26 +02:00
|
|
|
_ db: Database,
|
2022-02-17 01:40:35 +01:00
|
|
|
senderSessionId: String,
|
|
|
|
threadId: String?,
|
2022-04-06 07:43:26 +02:00
|
|
|
forceConfigSync: Bool
|
2022-04-21 08:42:35 +02:00
|
|
|
) throws {
|
2022-04-06 07:43:26 +02:00
|
|
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
2022-02-02 06:59:56 +01:00
|
|
|
|
2022-02-17 01:40:35 +01:00
|
|
|
// If the sender of the message was the current user
|
|
|
|
if senderSessionId == userPublicKey {
|
2022-04-21 08:42:35 +02:00
|
|
|
// Retrieve the contact for the thread the message was sent to (excluding 'NoteToSelf'
|
|
|
|
// threads) and if the contact isn't flagged as approved then do so
|
|
|
|
guard
|
|
|
|
let threadId: String = threadId,
|
|
|
|
let thread: SessionThread = try? SessionThread.fetchOne(db, id: threadId),
|
|
|
|
!thread.isNoteToSelf(db),
|
|
|
|
let contact: Contact = try? thread.contact.fetchOne(db),
|
|
|
|
!contact.isApproved
|
|
|
|
else { return }
|
2022-02-17 01:40:35 +01:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
try? contact
|
|
|
|
.with(isApproved: true)
|
|
|
|
.update(db)
|
2022-02-17 01:40:35 +01:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
// The message was sent to the current user so flag their 'didApproveMe' as true (can't send a message to
|
|
|
|
// someone without approving them)
|
2022-04-21 08:42:35 +02:00
|
|
|
guard
|
|
|
|
let contact: Contact = try? Contact.fetchOne(db, id: senderSessionId),
|
|
|
|
!contact.didApproveMe
|
|
|
|
else { return }
|
|
|
|
|
|
|
|
try? contact
|
|
|
|
.with(didApproveMe: true)
|
|
|
|
.update(db)
|
2022-02-17 01:40:35 +01:00
|
|
|
}
|
2022-02-02 06:59:56 +01:00
|
|
|
|
2022-03-24 00:03:51 +01:00
|
|
|
// Force a config sync to ensure all devices know the contact approval state if desired
|
2022-02-17 01:40:35 +01:00
|
|
|
guard forceConfigSync else { return }
|
2022-02-02 06:59:56 +01:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
2022-02-02 06:59:56 +01:00
|
|
|
}
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
private static func handleMessageRequestResponse(_ db: Database, _ message: MessageRequestResponse) throws {
|
2022-04-06 07:43:26 +02:00
|
|
|
let userPublicKey = getUserHexEncodedPublicKey(db)
|
2022-02-02 06:59:56 +01:00
|
|
|
|
2022-02-11 00:47:27 +01:00
|
|
|
// Ignore messages which were sent from the current user
|
|
|
|
guard message.sender != userPublicKey else { return }
|
2022-02-02 06:59:56 +01:00
|
|
|
guard let senderId: String = message.sender else { return }
|
|
|
|
|
|
|
|
// Get the existing thead and notify the user
|
2022-04-21 08:42:35 +02:00
|
|
|
if let thread: SessionThread = try? SessionThread.fetchOne(db, id: senderId) {
|
|
|
|
_ = try Interaction(
|
2022-05-08 14:01:39 +02:00
|
|
|
serverHash: message.serverHash,
|
2022-04-21 08:42:35 +02:00
|
|
|
threadId: thread.id,
|
|
|
|
authorId: senderId,
|
|
|
|
variant: .infoMessageRequestAccepted,
|
|
|
|
timestampMs: (
|
|
|
|
message.sentTimestamp.map { Int64($0) } ??
|
|
|
|
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
|
|
|
)
|
|
|
|
).inserted(db)
|
2022-02-02 06:59:56 +01:00
|
|
|
}
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
try updateContactApprovalStatusIfNeeded(
|
2022-04-06 07:43:26 +02:00
|
|
|
db,
|
2022-02-17 01:40:35 +01:00
|
|
|
senderSessionId: senderId,
|
|
|
|
threadId: nil,
|
2022-04-06 07:43:26 +02:00
|
|
|
forceConfigSync: true
|
2022-02-02 06:59:56 +01:00
|
|
|
)
|
|
|
|
}
|
2020-11-18 05:53:45 +01:00
|
|
|
}
|