Merge pull request #622 from RyanRory/authentication-1

Message sending & retrieving authentication
pull/625/head
RyanZhao 2 years ago committed by GitHub
commit 306a6aa2f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -33,14 +33,14 @@ public final class BackgroundPoller : NSObject {
private static func pollForClosedGroupMessages() -> [Promise<Void>] {
let publicKeys = Storage.shared.getUserClosedGroupPublicKeys()
return publicKeys.map { getMessages(for: $0) }
return publicKeys.map { getClosedGroupMessages(for: $0) }
}
private static func getMessages(for publicKey: String) -> Promise<Void> {
return SnodeAPI.getSwarm(for: publicKey).then(on: DispatchQueue.main) { swarm -> Promise<Void> in
guard let snode = swarm.randomElement() else { throw SnodeAPI.Error.generic }
return attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.main) {
return SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey).then(on: DispatchQueue.main) { rawResponse -> Promise<Void> in
SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey).then(on: DispatchQueue.main) { rawResponse -> Promise<Void> in
let (messages, lastRawMessage) = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: publicKey)
let promises = messages.compactMap { json -> Promise<Void>? in
// Use a best attempt approach here; we don't want to fail the entire process if one of the
@ -50,13 +50,55 @@ public final class BackgroundPoller : NSObject {
let job = MessageReceiveJob(data: data, serverHash: json["hash"] as? String, isBackgroundPoll: true)
return job.execute()
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: lastRawMessage)
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, namespace: SnodeAPI.defaultNamespace, associatedWith: publicKey, from: lastRawMessage)
return when(fulfilled: promises) // The promise returned by MessageReceiveJob never rejects
}
}
}
}
private static func getClosedGroupMessages(for publicKey: String) -> Promise<Void> {
return SnodeAPI.getSwarm(for: publicKey).then(on: DispatchQueue.main) { swarm -> Promise<Void> in
guard let snode = swarm.randomElement() else { throw SnodeAPI.Error.generic }
return attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.main) {
var promises: [SnodeAPI.RawResponsePromise] = []
var namespaces: [Int] = []
// We have to poll for both namespace 0 and -10 when hardfork == 19 && softfork == 0
if SnodeAPI.hardfork <= 19, SnodeAPI.softfork == 0 {
let promise = SnodeAPI.getRawClosedGroupMessagesFromDefaultNamespace(from: snode, associatedWith: publicKey)
promises.append(promise)
namespaces.append(SnodeAPI.defaultNamespace)
}
if SnodeAPI.hardfork >= 19 && SnodeAPI.softfork >= 0 {
let promise = SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey, authenticated: false)
promises.append(promise)
namespaces.append(SnodeAPI.closedGroupNamespace)
}
return when(resolved: promises).then(on: DispatchQueue.main) { results -> Promise<Void> in
var promises: [Promise<Void>] = []
var index = 0
for result in results {
if case .fulfilled(let rawResponse) = result {
let (messages, lastRawMessage) = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: publicKey)
let jobPromises = messages.compactMap { json -> Promise<Void>? in
// Use a best attempt approach here; we don't want to fail the entire process if one of the
// messages failed to parse.
guard let envelope = SNProtoEnvelope.from(json),
let data = try? envelope.serializedData() else { return nil }
let job = MessageReceiveJob(data: data, serverHash: json["hash"] as? String, isBackgroundPoll: true)
return job.execute()
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, namespace: namespaces[index], associatedWith: publicKey, from: lastRawMessage)
promises += jobPromises
}
index += 1
}
return when(fulfilled: promises) // The promise returned by MessageReceiveJob never rejects
}
}
}
}
}

@ -204,7 +204,10 @@ public final class MessageSender : NSObject {
let base64EncodedData = wrappedMessage.base64EncodedString()
let timestamp = UInt64(Int64(message.sentTimestamp!) + SnodeAPI.clockOffset)
let snodeMessage = SnodeMessage(recipient: message.recipient!, data: base64EncodedData, ttl: message.ttl, timestamp: timestamp)
SnodeAPI.sendMessage(snodeMessage).done(on: DispatchQueue.global(qos: .userInitiated)) { promises in
SnodeAPI.sendMessage(snodeMessage,
isClosedGroupMessage: (kind == .closedGroupMessage),
isConfigMessage: message.isKind(of: ConfigurationMessage.self))
.done(on: DispatchQueue.global(qos: .userInitiated)) { promises in
var isSuccess = false
let promiseCount = promises.count
var errorCount = 0

@ -87,7 +87,14 @@ public final class ClosedGroupPoller : NSObject {
timers[groupPublicKey] = Timer.scheduledTimerOnMainThread(withTimeInterval: nextPollInterval, repeats: false) { [weak self] timer in
timer.invalidate()
Threading.pollerQueue.async {
self?.poll(groupPublicKey).done(on: Threading.pollerQueue) { _ in
var promises: [Promise<Void>] = []
if SnodeAPI.hardfork <= 19, SnodeAPI.softfork == 0, let promise = self?.poll(groupPublicKey, defaultInbox: true) {
promises.append(promise)
}
if SnodeAPI.hardfork >= 19, SnodeAPI.softfork >= 0,let promise = self?.poll(groupPublicKey) {
promises.append(promise)
}
when(resolved: promises).done(on: Threading.pollerQueue) { _ in
self?.pollRecursively(groupPublicKey)
}.catch(on: Threading.pollerQueue) { error in
// The error is logged in poll(_:)
@ -97,13 +104,14 @@ public final class ClosedGroupPoller : NSObject {
}
}
private func poll(_ groupPublicKey: String) -> Promise<Void> {
private func poll(_ groupPublicKey: String, defaultInbox: Bool = false) -> Promise<Void> {
guard isPolling(for: groupPublicKey) else { return Promise.value(()) }
let promise = SnodeAPI.getSwarm(for: groupPublicKey).then2 { [weak self] swarm -> Promise<(Snode, [JSON], JSON?)> in
// randomElement() uses the system's default random generator, which is cryptographically secure
guard let snode = swarm.randomElement() else { return Promise(error: Error.insufficientSnodes) }
guard let self = self, self.isPolling(for: groupPublicKey) else { return Promise(error: Error.pollingCanceled) }
return SnodeAPI.getRawMessages(from: snode, associatedWith: groupPublicKey).map2 {
let getRawMessagesPromise = defaultInbox ? SnodeAPI.getRawClosedGroupMessagesFromDefaultNamespace(from: snode, associatedWith: groupPublicKey) : SnodeAPI.getRawMessages(from: snode, associatedWith: groupPublicKey, authenticated: false)
return getRawMessagesPromise.map2 {
let (rawMessages, lastRawMessage) = SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: groupPublicKey)
return (snode, rawMessages, lastRawMessage)
@ -128,7 +136,7 @@ public final class ClosedGroupPoller : NSObject {
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, associatedWith: groupPublicKey, from: lastRawMessage)
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, namespace: SnodeAPI.closedGroupNamespace, associatedWith: groupPublicKey, from: lastRawMessage)
}
promise.catch2 { error in
SNLog("Polling failed for closed group with public key: \(groupPublicKey) due to error: \(error).")

@ -112,7 +112,7 @@ public final class Poller : NSObject {
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, associatedWith: userPublicKey, from: lastRawMessage)
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, namespace: SnodeAPI.defaultNamespace, associatedWith: userPublicKey, from: lastRawMessage)
strongSelf.pollCount += 1
if strongSelf.pollCount == Poller.maxPollCount {

@ -395,6 +395,9 @@ public enum OnionRequestAPI {
if statusCode == 406 { // Clock out of sync
SNLog("The user's clock is out of sync with the service node network.")
seal.reject(SnodeAPI.Error.clockOutOfSync)
} else if statusCode == 401 { // Signature verification failed
SNLog("Failed to verify the signature.")
seal.reject(SnodeAPI.Error.signatureVerificationFailed)
} else if let bodyAsString = json["body"] as? String {
guard let bodyAsData = bodyAsString.data(using: .utf8),
let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) }

@ -22,6 +22,15 @@ public final class SnodeAPI : NSObject {
public static var clockOffset: Int64 = 0
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
public static var swarmCache: [String:Set<Snode>] = [:]
// MARK: Namespaces
public static let defaultNamespace = 0
public static let closedGroupNamespace = -10
public static let configNamespace = 5
// MARK: Hardfork version
public static var hardfork = UserDefaults.standard[.hardfork]
public static var softfork = UserDefaults.standard[.softfork]
// MARK: Settings
private static let maxRetryCount: UInt = 8
@ -39,6 +48,7 @@ public final class SnodeAPI : NSObject {
case inconsistentSnodePools
case noKeyPair
case signingFailed
case signatureVerificationFailed
// ONS
case decryptionFailed
case hashingFailed
@ -52,6 +62,7 @@ public final class SnodeAPI : NSObject {
case .inconsistentSnodePools: return "Received inconsistent Service Node pool information from the Service Node network."
case .noKeyPair: return "Missing user key pair."
case .signingFailed: return "Couldn't sign message."
case . signatureVerificationFailed: return "Failed to verify the signature."
// ONS
case .decryptionFailed: return "Couldn't decrypt ONS name."
case .hashingFailed: return "Couldn't compute ONS name hash."
@ -131,7 +142,22 @@ public final class SnodeAPI : NSObject {
// MARK: Internal API
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise {
if Features.useOnionRequests {
return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey)
.map2 { json in
if let hf = json["hf"] as? [Int] {
if hf[1] > softfork {
softfork = hf[1]
UserDefaults.standard[.softfork] = softfork
}
if hf[0] > hardfork {
hardfork = hf[0]
UserDefaults.standard[.hardfork] = hardfork
softfork = hf[1]
UserDefaults.standard[.softfork] = softfork
}
}
return json as Any
}
} else {
let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in
@ -390,47 +416,114 @@ public final class SnodeAPI : NSObject {
}
}
public static func getRawMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
// MARK: Retrieve
// Not in use until we can batch delete and store config messages
public static func getConfigMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
let (promise, seal) = RawResponsePromise.pending()
Threading.workQueue.async {
getMessagesWithAuthentication(from: snode, associatedWith: publicKey, namespace: configNamespace).done2 {
seal.fulfill($0)
}.catch2 {
seal.reject($0)
}
}
return promise
}
public static func getRawMessages(from snode: Snode, associatedWith publicKey: String, authenticated: Bool = true) -> RawResponsePromise {
let (promise, seal) = RawResponsePromise.pending()
Threading.workQueue.async {
getMessagesInternal(from: snode, associatedWith: publicKey).done2 { seal.fulfill($0) }.catch2 { seal.reject($0) }
let retrievePromise = authenticated ? getMessagesWithAuthentication(from: snode, associatedWith: publicKey, namespace: defaultNamespace) : getMessagesUnauthenticated(from: snode, associatedWith: publicKey)
retrievePromise.done2 { seal.fulfill($0) }.catch2 { seal.reject($0) }
}
return promise
}
private static func getMessagesInternal(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
public static func getRawClosedGroupMessagesFromDefaultNamespace(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
let (promise, seal) = RawResponsePromise.pending()
Threading.workQueue.async {
getMessagesUnauthenticated(from: snode, associatedWith: publicKey, namespace: defaultNamespace).done2 { seal.fulfill($0) }.catch2 { seal.reject($0) }
}
return promise
}
private static func getMessagesWithAuthentication(from snode: Snode, associatedWith publicKey: String, namespace: Int) -> RawResponsePromise {
let storage = SNSnodeKitConfiguration.shared.storage
// NOTE: All authentication logic is currently commented out, the reason being that we can't currently support
// NOTE: All authentication logic is only apply to 1-1 chats, the reason being that we can't currently support
// it yet for closed groups. The Storage Server requires an ed25519 key pair, but we don't have that for our
// closed groups.
// guard let userED25519KeyPair = storage.getUserED25519KeyPair() else { return Promise(error: Error.noKeyPair) }
guard let userED25519KeyPair = storage.getUserED25519KeyPair() else { return Promise(error: Error.noKeyPair) }
// Get last message hash
storage.pruneLastMessageHashInfoIfExpired(for: snode, associatedWith: publicKey)
let lastHash = storage.getLastMessageHash(for: snode, associatedWith: publicKey) ?? ""
storage.pruneLastMessageHashInfoIfExpired(for: snode, namespace: namespace, associatedWith: publicKey)
let lastHash = storage.getLastMessageHash(for: snode, namespace: namespace, associatedWith: publicKey) ?? ""
// Construct signature
// let timestamp = UInt64(Int64(NSDate.millisecondTimestamp()) + SnodeAPI.clockOffset)
// let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString()
// let verificationData = ("retrieve" + String(timestamp)).data(using: String.Encoding.utf8)!
// let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey)!
let timestamp = UInt64(Int64(NSDate.millisecondTimestamp()) + SnodeAPI.clockOffset)
let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString()
let namespaceVerificationString = namespace == defaultNamespace ? "" : String(namespace)
guard let verificationData = ("retrieve" + namespaceVerificationString + String(timestamp)).data(using: String.Encoding.utf8),
let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey)
else { return Promise(error: Error.signingFailed) }
// Make the request
let parameters: JSON = [
"pubKey" : Features.useTestnet ? publicKey.removing05PrefixIfNeeded() : publicKey,
"namespace": namespace,
"lastHash" : lastHash,
// "timestamp" : timestamp,
// "pubkey_ed25519" : ed25519PublicKey,
// "signature" : signature.toBase64()!
"timestamp" : timestamp,
"pubkey_ed25519" : ed25519PublicKey,
"signature" : signature.toBase64()
]
return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters)
}
public static func sendMessage(_ message: SnodeMessage) -> Promise<Set<RawResponsePromise>> {
private static func getMessagesUnauthenticated(from snode: Snode, associatedWith publicKey: String, namespace: Int = closedGroupNamespace) -> RawResponsePromise {
let storage = SNSnodeKitConfiguration.shared.storage
// Get last message hash
storage.pruneLastMessageHashInfoIfExpired(for: snode, namespace: namespace, associatedWith: publicKey)
let lastHash = storage.getLastMessageHash(for: snode, namespace: namespace, associatedWith: publicKey) ?? ""
// Make the request
var parameters: JSON = [
"pubKey" : Features.useTestnet ? publicKey.removing05PrefixIfNeeded() : publicKey,
"lastHash" : lastHash,
]
// Don't include namespace if polling for 0 with no authentication
if namespace != defaultNamespace {
parameters["namespace"] = namespace
}
return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters)
}
// MARK: Store
public static func sendMessage(_ message: SnodeMessage, isClosedGroupMessage: Bool, isConfigMessage: Bool) -> Promise<Set<RawResponsePromise>> {
let namespace = isClosedGroupMessage ? closedGroupNamespace : defaultNamespace
return sendMessageUnauthenticated(message, namespace: namespace)
}
// Not in use until we can batch delete and store config messages
private static func sendMessageWithAuthentication(_ message: SnodeMessage, namespace: Int) -> Promise<Set<RawResponsePromise>> {
let storage = SNSnodeKitConfiguration.shared.storage
guard let userED25519KeyPair = storage.getUserED25519KeyPair() else { return Promise(error: Error.noKeyPair) }
// Construct signature
let timestamp = UInt64(Int64(NSDate.millisecondTimestamp()) + SnodeAPI.clockOffset)
let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString()
guard let verificationData = ("store" + String(namespace) + String(timestamp)).data(using: String.Encoding.utf8),
let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey)
else { return Promise(error: Error.signingFailed) }
// Make the request
let (promise, seal) = Promise<Set<RawResponsePromise>>.pending()
let publicKey = Features.useTestnet ? message.recipient.removing05PrefixIfNeeded() : message.recipient
Threading.workQueue.async {
getTargetSnodes(for: publicKey).map2 { targetSnodes in
let parameters = message.toJSON()
var parameters = message.toJSON()
parameters["namespace"] = namespace
parameters["sig_timestamp"] = timestamp
parameters["pubkey_ed25519"] = ed25519PublicKey
parameters["signature"] = signature.toBase64()
return Set(targetSnodes.map { targetSnode in
attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters)
@ -441,6 +534,25 @@ public final class SnodeAPI : NSObject {
return promise
}
private static func sendMessageUnauthenticated(_ message: SnodeMessage, namespace: Int) -> Promise<Set<RawResponsePromise>> {
let (promise, seal) = Promise<Set<RawResponsePromise>>.pending()
let publicKey = Features.useTestnet ? message.recipient.removing05PrefixIfNeeded() : message.recipient
Threading.workQueue.async {
getTargetSnodes(for: publicKey).map2 { targetSnodes in
var parameters = message.toJSON()
parameters["namespace"] = namespace
return Set(targetSnodes.map { targetSnode in
attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters)
}
})
}.done2 { seal.fulfill($0) }.catch2 { seal.reject($0) }
}
return promise
}
// MARK: Delete
@objc(deleteMessageForPublickKey:serverHashes:)
public static func objc_deleteMessage(publicKey: String, serverHashes: [String]) -> AnyPromise {
AnyPromise.from(deleteMessage(publicKey: publicKey, serverHashes: serverHashes))
@ -568,10 +680,10 @@ public final class SnodeAPI : NSObject {
)
}
public static func updateLastMessageHashValueIfPossible(for snode: Snode, associatedWith publicKey: String, from lastRawMessage: JSON?) {
public static func updateLastMessageHashValueIfPossible(for snode: Snode, namespace: Int, associatedWith publicKey: String, from lastRawMessage: JSON?) {
if let lastMessage = lastRawMessage, let lastHash = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? UInt64 {
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
SNSnodeKitConfiguration.shared.storage.setLastMessageHashInfo(for: snode, associatedWith: publicKey,
SNSnodeKitConfiguration.shared.storage.setLastMessageHashInfo(for: snode, namespace: namespace, associatedWith: publicKey,
to: [ "hash" : lastHash, "expirationDate" : NSNumber(value: expirationDate) ], using: transaction)
}
} else if (lastRawMessage != nil) {

@ -80,8 +80,8 @@ extension Storage {
private static let lastMessageHashCollection = "LokiLastMessageHashCollection"
public func getLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String) -> JSON? {
let key = "\(snode.address):\(snode.port).\(publicKey)"
public func getLastMessageHashInfo(for snode: Snode, namespace: Int, associatedWith publicKey: String) -> JSON? {
let key = namespace == SnodeAPI.defaultNamespace ? "\(snode.address):\(snode.port).\(publicKey)" : "\(snode.address):\(snode.port).\(publicKey).\(namespace)"
var result: JSON?
Storage.read { transaction in
result = transaction.object(forKey: key, inCollection: Storage.lastMessageHashCollection) as? JSON
@ -93,30 +93,30 @@ extension Storage {
return result
}
public func getLastMessageHash(for snode: Snode, associatedWith publicKey: String) -> String? {
return getLastMessageHashInfo(for: snode, associatedWith: publicKey)?["hash"] as? String
public func getLastMessageHash(for snode: Snode, namespace: Int, associatedWith publicKey: String) -> String? {
return getLastMessageHashInfo(for: snode, namespace: namespace, associatedWith: publicKey)?["hash"] as? String
}
public func setLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any) {
let key = "\(snode.address):\(snode.port).\(publicKey)"
public func setLastMessageHashInfo(for snode: Snode, namespace: Int, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any) {
let key = namespace == SnodeAPI.defaultNamespace ? "\(snode.address):\(snode.port).\(publicKey)" : "\(snode.address):\(snode.port).\(publicKey).\(namespace)"
guard lastMessageHashInfo.count == 2 && lastMessageHashInfo["hash"] as? String != nil && lastMessageHashInfo["expirationDate"] as? NSNumber != nil else { return }
(transaction as! YapDatabaseReadWriteTransaction).setObject(lastMessageHashInfo, forKey: key, inCollection: Storage.lastMessageHashCollection)
}
public func pruneLastMessageHashInfoIfExpired(for snode: Snode, associatedWith publicKey: String) {
guard let lastMessageHashInfo = getLastMessageHashInfo(for: snode, associatedWith: publicKey),
public func pruneLastMessageHashInfoIfExpired(for snode: Snode, namespace: Int, associatedWith publicKey: String) {
guard let lastMessageHashInfo = getLastMessageHashInfo(for: snode, namespace: namespace, associatedWith: publicKey),
(lastMessageHashInfo["hash"] as? String) != nil, let expirationDate = (lastMessageHashInfo["expirationDate"] as? NSNumber)?.uint64Value else { return }
let now = NSDate.millisecondTimestamp()
if now >= expirationDate {
Storage.writeSync { transaction in
self.removeLastMessageHashInfo(for: snode, associatedWith: publicKey, using: transaction)
self.removeLastMessageHashInfo(for: snode, namespace: namespace, associatedWith: publicKey, using: transaction)
self.setReceivedMessages(to: Set(), for: publicKey, using: transaction)
}
}
}
public func removeLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, using transaction: Any) {
let key = "\(snode.address):\(snode.port).\(publicKey)"
public func removeLastMessageHashInfo(for snode: Snode, namespace: Int, associatedWith publicKey: String, using transaction: Any) {
let key = namespace == SnodeAPI.defaultNamespace ? "\(snode.address):\(snode.port).\(publicKey)" : "\(snode.address):\(snode.port).\(publicKey).\(namespace)"
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: Storage.lastMessageHashCollection)
}

@ -20,9 +20,9 @@ public protocol SessionSnodeKitStorageProtocol {
func setLastSnodePoolRefreshDate(to date: Date, using transaction: Any)
func getSwarm(for publicKey: String) -> Set<Snode>
func setSwarm(to swarm: Set<Snode>, for publicKey: String, using transaction: Any)
func getLastMessageHash(for snode: Snode, associatedWith publicKey: String) -> String?
func setLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any)
func pruneLastMessageHashInfoIfExpired(for snode: Snode, associatedWith publicKey: String)
func getLastMessageHash(for snode: Snode, namespace: Int, associatedWith publicKey: String) -> String?
func setLastMessageHashInfo(for snode: Snode, namespace: Int, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any)
func pruneLastMessageHashInfoIfExpired(for snode: Snode, namespace: Int, associatedWith publicKey: String)
func getReceivedMessages(for publicKey: String) -> Set<String>
func setReceivedMessages(to receivedMessages: Set<String>, for publicKey: String, using transaction: Any)
}

@ -26,6 +26,8 @@ public enum SNUserDefaults {
public enum Int : Swift.String {
case appMode
case hardfork
case softfork
}
public enum String : Swift.String {

Loading…
Cancel
Save