// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // // stringlint:disable import Foundation import GRDB import SessionUtilitiesKit public struct SnodeReceivedMessageInfo: Codable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "snodeReceivedMessageInfo" } public typealias Columns = CodingKeys public enum CodingKeys: String, CodingKey, ColumnExpression { case swarmPublicKey case snodeAddress case namespace case hash case expirationDateMs case wasDeletedOrInvalid } /// The public key for the swarm this message info was retrieved from public let swarmPublicKey: String /// The address for the snode this message info was retrieved from (in the form of `{server}:{port}`) public let snodeAddress: String /// The namespace this message info was retrieved from public let namespace: Int /// The is the hash for the received message public let hash: String /// This is the timestamp (in milliseconds since epoch) when the message hash should expire /// /// **Note:** If no value exists this will default to 15 days from now (since the service node caches messages for /// 14 days for standard messages) public let expirationDateMs: Int64 /// This flag indicates whether the message associated with this message hash was deleted or whether this message /// hash is potentially invalid (if a poll results in 100% of the `SnodeReceivedMessageInfo` entries being seen as /// duplicates then we assume that the `lastHash` value provided when retrieving messages was invalid and mark /// it as such) /// /// This flag can also be used to refetch messages from a swarm without impacting the hash-based deduping mechanism /// as if a hash with this value set to `true` is received when pollig then the value gets reset to `false` /// /// **Note:** When retrieving the `lastNotExpired` we will ignore any entries where this flag is `true` public var wasDeletedOrInvalid: Bool } // MARK: - Convenience public extension SnodeReceivedMessageInfo { init( snode: LibSession.Snode, swarmPublicKey: String, namespace: SnodeAPI.Namespace, hash: String, expirationDateMs: Int64? ) { self.swarmPublicKey = swarmPublicKey self.snodeAddress = snode.address self.namespace = namespace.rawValue self.hash = hash self.expirationDateMs = (expirationDateMs ?? 0) self.wasDeletedOrInvalid = false } } // MARK: - GRDB Interactions public extension SnodeReceivedMessageInfo { /// This method fetches the last non-expired hash from the database for message retrieval static func fetchLastNotExpired( _ db: Database, for snode: LibSession.Snode, namespace: SnodeAPI.Namespace, swarmPublicKey: String, using dependencies: Dependencies ) throws -> SnodeReceivedMessageInfo? { let currentOffsetTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() return try SnodeReceivedMessageInfo .filter(SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid == false) .filter( SnodeReceivedMessageInfo.Columns.swarmPublicKey == swarmPublicKey && SnodeReceivedMessageInfo.Columns.snodeAddress == snode.address && SnodeReceivedMessageInfo.Columns.namespace == namespace.rawValue ) .filter(SnodeReceivedMessageInfo.Columns.expirationDateMs > currentOffsetTimestampMs) .order(Column.rowID.desc) .fetchOne(db) } /// There are some cases where the latest message can be removed from a swarm, if we then try to poll for that message the swarm /// will see it as invalid and start returning messages from the beginning which can result in a lot of wasted, duplicate downloads /// /// This method should be called when deleting a message, handling an UnsendRequest or when receiving a poll response which contains /// solely duplicate messages (for the specific service node - if even one message in a response is new for that service node then this shouldn't /// be called if if the message has already been received and processed by a separate service node) static func handlePotentialDeletedOrInvalidHash( _ db: Database, potentiallyInvalidHashes: [String], otherKnownValidHashes: [String] = [] ) throws { if !potentiallyInvalidHashes.isEmpty { _ = try SnodeReceivedMessageInfo .filter(potentiallyInvalidHashes.contains(SnodeReceivedMessageInfo.Columns.hash)) .updateAll( db, SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid.set(to: true) ) } // If we have any server hashes which we know are valid (eg. we fetched the oldest messages) then // mark them all as valid to prevent the case where we just slowly work backwards from the latest // message, polling for one earlier each time if !otherKnownValidHashes.isEmpty { _ = try SnodeReceivedMessageInfo .filter(otherKnownValidHashes.contains(SnodeReceivedMessageInfo.Columns.hash)) .updateAll( db, SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid.set(to: false) ) } } }