Merge remote-tracking branch 'origin/fix/sync-message-issues' into feature/lib-quic-integration

# Conflicts:
#	.drone.jsonnet
#	SessionSnodeKit/Networking/SnodeAPI.swift
#	SessionSnodeKit/Types/OnionRequestAPIError.swift
#	SessionSnodeKit/Types/SnodeAPIError.swift
pull/960/head
Morgan Pretty 12 months ago
commit 8c467dc511

@ -2861,11 +2861,11 @@
C300A5BB2554AFFB00555489 /* Messages */ = {
isa = PBXGroup;
children = (
C300A5C62554B02D00555489 /* Visible Messages */,
C300A5C72554B03900555489 /* Control Messages */,
C3C2A74325539EB700C340D1 /* Message.swift */,
C352A30825574D8400338F3E /* Message+Destination.swift */,
943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */,
C300A5C62554B02D00555489 /* Visible Messages */,
C300A5C72554B03900555489 /* Control Messages */,
);
path = Messages;
sourceTree = "<group>";
@ -7364,6 +7364,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SignalUtilitiesKit";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
STRIP_INSTALLED_PRODUCT = NO;
SUPPORTS_MACCATALYST = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -7533,6 +7534,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
STRIP_INSTALLED_PRODUCT = NO;
SUPPORTS_MACCATALYST = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_INCLUDE_PATHS = "$(inherited) \"${PODS_XCFRAMEWORKS_BUILD_DIR}/Clibsodium\" \"$(TARGET_BUILD_DIR)/libSessionUtil\"";
@ -7917,6 +7919,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionMessagingKit";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
STRIP_INSTALLED_PRODUCT = NO;
SUPPORTS_MACCATALYST = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_INCLUDE_PATHS = "$(inherited) \"${PODS_XCFRAMEWORKS_BUILD_DIR}/Clibsodium\" \"$(TARGET_BUILD_DIR)/libSessionUtil\"";
@ -8025,6 +8028,7 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
STRIP_INSTALLED_PRODUCT = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_INCLUDE_PATHS = "$(inherited) \"${PODS_XCFRAMEWORKS_BUILD_DIR}/Clibsodium\" \"$(TARGET_BUILD_DIR)/libSessionUtil\"";
@ -8249,6 +8253,7 @@
PROVISIONING_PROFILE_SPECIFIER = "";
RUN_CLANG_STATIC_ANALYZER = YES;
SDKROOT = iphoneos;
STRIP_INSTALLED_PRODUCT = NO;
SWIFT_OBJC_BRIDGING_HEADER = "Session/Meta/Signal-Bridging-Header.h";
SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -8665,6 +8670,7 @@
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)";
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
@ -8717,7 +8723,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)";
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;

@ -224,7 +224,7 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
let message: String = {
if let error = error as? SnodeAPIError {
switch error {
case .decryptionFailed, .hashingFailed, .validationFailed:
case .onsDecryptionFailed, .onsHashingFailed, .onsValidationFailed:
return "\(error)"
default: break

@ -63,8 +63,8 @@ public enum AttachmentUploadJob: JobExecutor {
MessageSender.handleMessageWillSend(
db,
message: details.message,
interactionId: interactionId,
isSyncMessage: details.isSyncMessage
destination: details.destination,
interactionId: interactionId
)
}
@ -93,9 +93,9 @@ public enum AttachmentUploadJob: JobExecutor {
MessageSender.handleFailedMessageSend(
db,
message: details.message,
destination: nil,
with: .other(error),
interactionId: interactionId,
isSyncMessage: details.isSyncMessage,
using: dependencies
)
}

@ -45,10 +45,8 @@ public enum GetExpirationJob: JobExecutor {
let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
SnodeAPI
.getSwarm(for: userPublicKey, using: dependencies)
.tryFlatMap { swarm -> AnyPublisher<(ResponseInfoType, GetExpiriesResponse), Error> in
guard let snode = swarm.randomElement() else { throw SnodeAPIError.ranOutOfRandomSnodes }
return SnodeAPI.getExpiries(
.tryFlatMapWithRandomSnode(using: dependencies) { snode -> AnyPublisher<(ResponseInfoType, GetExpiriesResponse), Error> in
SnodeAPI.getExpiries(
from: snode,
swarmPublicKey: userPublicKey,
of: expirationInfo.map { $0.key },

@ -51,7 +51,6 @@ public enum GroupLeavingJob: JobExecutor {
to: destination,
namespace: destination.defaultNamespace,
interactionId: job.interactionId,
isSyncMessage: false,
using: dependencies
)
}

@ -176,7 +176,6 @@ public enum MessageSendJob: JobExecutor {
to: details.destination,
namespace: details.destination.defaultNamespace,
interactionId: job.interactionId,
isSyncMessage: details.isSyncMessage,
using: dependencies
)
}
@ -197,7 +196,7 @@ public enum MessageSendJob: JobExecutor {
SNLog("[MessageSendJob] Couldn't send message due to error: \(error) (paths: \(OnionRequestAPI.paths.prettifiedDescription)).")
default:
SNLog("[MessageSendJob] Couldn't send message due to error: \(error).")
SNLog("[MessageSendJob] Couldn't send message due to error: \(error)")
}
// Actual error handling
@ -240,25 +239,22 @@ extension MessageSendJob {
private enum CodingKeys: String, CodingKey {
case destination
case message
case isSyncMessage
@available(*, deprecated, message: "replaced by 'Message.Destination.syncMessage'") case isSyncMessage
case variant
}
public let destination: Message.Destination
public let message: Message
public let isSyncMessage: Bool
public let variant: Message.Variant?
// MARK: - Initialization
public init(
destination: Message.Destination,
message: Message,
isSyncMessage: Bool = false
message: Message
) {
self.destination = destination
self.message = message
self.isSyncMessage = isSyncMessage
self.variant = Message.Variant(from: message)
}
@ -272,10 +268,42 @@ extension MessageSendJob {
throw StorageError.decodingFailed
}
let message: Message = try variant.decode(from: container, forKey: .message)
var destination: Message.Destination = try container.decode(Message.Destination.self, forKey: .destination)
/// Handle the legacy 'isSyncMessage' flag - this flag was deprecated in `2.5.2` (April 2024) and can be removed in a
/// subsequent release after May 2024
if ((try? container.decode(Bool.self, forKey: .isSyncMessage)) ?? false) {
switch (destination, message) {
case (.contact, let message as VisibleMessage):
guard let targetPublicKey: String = message.syncTarget else {
SNLog("Unable to decode messageSend job due to missing syncTarget")
throw StorageError.decodingFailed
}
destination = .syncMessage(originalRecipientPublicKey: targetPublicKey)
case (.contact, let message as ExpirationTimerUpdate):
guard let targetPublicKey: String = message.syncTarget else {
SNLog("Unable to decode messageSend job due to missing syncTarget")
throw StorageError.decodingFailed
}
destination = .syncMessage(originalRecipientPublicKey: targetPublicKey)
case (.contact(let publicKey), _):
SNLog("Sync message in messageSend job was missing explicit syncTarget (falling back to specified value)")
destination = .syncMessage(originalRecipientPublicKey: publicKey)
default:
SNLog("Unable to decode messageSend job due to invalid sync message state")
throw StorageError.decodingFailed
}
}
self = Details(
destination: try container.decode(Message.Destination.self, forKey: .destination),
message: try variant.decode(from: container, forKey: .message),
isSyncMessage: ((try? container.decode(Bool.self, forKey: .isSyncMessage)) ?? false)
destination: destination,
message: message
)
}
@ -289,7 +317,6 @@ extension MessageSendJob {
try container.encode(destination, forKey: .destination)
try container.encode(message, forKey: .message)
try container.encode(isSyncMessage, forKey: .isSyncMessage)
try container.encode(variant, forKey: .variant)
}
}

@ -43,8 +43,7 @@ public enum SendReadReceiptsJob: JobExecutor {
),
to: details.destination,
namespace: details.destination.defaultNamespace,
interactionId: nil,
isSyncMessage: false
interactionId: nil
)
}
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }

@ -7,8 +7,16 @@ import SessionUtilitiesKit
public extension Message {
enum Destination: Codable, Hashable {
/// A message directed to another user
case contact(publicKey: String)
/// A message that was originally sent to another user but needs to be replicated to the current users swarm
case syncMessage(originalRecipientPublicKey: String)
/// A message directed to group conversation
case closedGroup(groupPublicKey: String)
/// A message directed to an open group
case openGroup(
roomToken: String,
server: String,
@ -16,13 +24,15 @@ public extension Message {
whisperMods: Bool = false,
fileIds: [String]? = nil
)
/// A message directed to an open group inbox
case openGroupInbox(server: String, openGroupPublicKey: String, blindedPublicKey: String)
public var defaultNamespace: SnodeAPI.Namespace? {
switch self {
case .contact: return .`default`
case .contact, .syncMessage: return .`default`
case .closedGroup: return .legacyClosedGroup
default: return nil
case .openGroup, .openGroupInbox: return nil
}
}

@ -246,7 +246,7 @@ public extension Message {
static func threadId(forMessage message: Message, destination: Message.Destination) -> String {
switch destination {
case .contact(let publicKey):
case .contact(let publicKey), .syncMessage(let publicKey):
// Extract the 'syncTarget' value if there is one
let maybeSyncTarget: String?
@ -661,29 +661,24 @@ public extension Message {
internal static func getSpecifiedTTL(
message: Message,
isGroupMessage: Bool,
isSyncMessage: Bool
destination: Message.Destination
) -> UInt64 {
guard Features.useNewDisappearingMessagesConfig else { return message.ttl }
// Not disappearing messages
guard let expiresInSeconds = message.expiresInSeconds else { return message.ttl }
// Sync message should be read already, it is the same for disappear after read and disappear after sent
guard !isSyncMessage else { return UInt64(expiresInSeconds * 1000) }
// Disappear after read messages that have not be read
guard let expiresStartedAtMs = message.expiresStartedAtMs else { return message.ttl }
// Disappear after read messages that have already be read
guard message.sentTimestamp == UInt64(expiresStartedAtMs) else { return message.ttl }
// Disappear after sent messages with exceptions
switch message {
case is ClosedGroupControlMessage, is UnsendRequest:
switch (destination, message) {
// Disappear after sent messages with exceptions
case (_, is UnsendRequest): return message.ttl
case (.closedGroup, is ClosedGroupControlMessage), (.closedGroup, is ExpirationTimerUpdate):
return message.ttl
case is ExpirationTimerUpdate:
return isGroupMessage ? message.ttl : UInt64(expiresInSeconds * 1000)
default:
guard
let expiresInSeconds = message.expiresInSeconds, // Not disappearing messages
expiresInSeconds > 0, // Not disappearing messages (0 == disabled)
let expiresStartedAtMs = message.expiresStartedAtMs, // Unread disappear after read message
message.sentTimestamp == UInt64(expiresStartedAtMs) // Already read disappearing messages
else { return message.ttl }
return UInt64(expiresInSeconds * 1000)
}
}

@ -4,7 +4,7 @@
import Foundation
public enum MessageSenderError: LocalizedError, Equatable {
public enum MessageSenderError: Error, CustomStringConvertible, Equatable {
case invalidMessage
case protoConversionFailed
case noUserX25519KeyPair
@ -33,24 +33,28 @@ public enum MessageSenderError: LocalizedError, Equatable {
}
}
public var errorDescription: String? {
public var description: String {
switch self {
case .invalidMessage: return "Invalid message."
case .protoConversionFailed: return "Couldn't convert message to proto."
case .noUserX25519KeyPair: return "Couldn't find user X25519 key pair."
case .noUserED25519KeyPair: return "Couldn't find user ED25519 key pair."
case .signingFailed: return "Couldn't sign message."
case .encryptionFailed: return "Couldn't encrypt message."
case .noUsername: return "Missing username."
case .attachmentsNotUploaded: return "Attachments for this message have not been uploaded."
case .blindingFailed: return "Couldn't blind the sender"
case .sendJobTimeout: return "Send job timeout (likely due to path building taking too long)."
case .invalidMessage: return "Invalid message (MessageSenderError.invalidMessage)."
case .protoConversionFailed: return "Couldn't convert message to proto (MessageSenderError.protoConversionFailed)."
case .noUserX25519KeyPair: return "Couldn't find user X25519 key pair (MessageSenderError.noUserX25519KeyPair)."
case .noUserED25519KeyPair: return "Couldn't find user ED25519 key pair (MessageSenderError.noUserED25519KeyPair)."
case .signingFailed: return "Couldn't sign message (MessageSenderError.signingFailed)."
case .encryptionFailed: return "Couldn't encrypt message (MessageSenderError.encryptionFailed)."
case .noUsername: return "Missing username (MessageSenderError.noUsername)."
case .attachmentsNotUploaded: return "Attachments for this message have not been uploaded (MessageSenderError.attachmentsNotUploaded)."
case .blindingFailed: return "Couldn't blind the sender (MessageSenderError.blindingFailed)."
case .sendJobTimeout: return "Send job timeout (likely due to path building taking too long - MessageSenderError.sendJobTimeout)."
// Closed groups
case .noThread: return "Couldn't find a thread associated with the given group public key."
case .noKeyPair: return "Couldn't find a private key associated with the given group public key."
case .invalidClosedGroupUpdate: return "Invalid group update."
case .other(let error): return error.localizedDescription
case .noThread: return "Couldn't find a thread associated with the given group public key (MessageSenderError.noThread)."
case .noKeyPair: return "Couldn't find a private key associated with the given group public key (MessageSenderError.noKeyPair)."
case .invalidClosedGroupUpdate: return "Invalid group update (MessageSenderError.invalidClosedGroupUpdate)."
case .other(let error):
switch error {
case is CustomStringConvertible: return "\(error)"
default: return error.localizedDescription
}
}
}

@ -70,7 +70,6 @@ extension MessageSender {
destination: destination,
threadId: threadId,
interactionId: interactionId,
isAlreadySyncMessage: false,
using: dependencies
)
return
@ -84,8 +83,7 @@ extension MessageSender {
interactionId: interactionId,
details: MessageSendJob.Details(
destination: destination,
message: message,
isSyncMessage: isSyncMessage
message: message
)
),
canStartJob: true,
@ -132,6 +130,7 @@ extension MessageSender {
let threadId: String = {
switch preparedSendData.destination {
case .contact(let publicKey): return publicKey
case .syncMessage(let originalRecipientPublicKey): return originalRecipientPublicKey
case .closedGroup(let groupPublicKey): return groupPublicKey
case .openGroup(let roomToken, let server, _, _, _):
return OpenGroup.idFor(roomToken: roomToken, server: server)

@ -17,7 +17,6 @@ public final class MessageSender {
let message: Message?
let interactionId: Int64?
let isSyncMessage: Bool?
let totalAttachmentsUploaded: Int
let snodeMessage: SnodeMessage?
@ -30,7 +29,6 @@ public final class MessageSender {
destination: Message.Destination,
namespace: SnodeAPI.Namespace?,
interactionId: Int64?,
isSyncMessage: Bool?,
totalAttachmentsUploaded: Int = 0,
snodeMessage: SnodeMessage?,
plaintext: Data?,
@ -42,7 +40,6 @@ public final class MessageSender {
self.destination = destination
self.namespace = namespace
self.interactionId = interactionId
self.isSyncMessage = isSyncMessage
self.totalAttachmentsUploaded = totalAttachmentsUploaded
self.snodeMessage = snodeMessage
@ -56,7 +53,6 @@ public final class MessageSender {
destination: Message.Destination,
namespace: SnodeAPI.Namespace,
interactionId: Int64?,
isSyncMessage: Bool?,
snodeMessage: SnodeMessage
) {
self.shouldSend = true
@ -65,7 +61,6 @@ public final class MessageSender {
self.destination = destination
self.namespace = namespace
self.interactionId = interactionId
self.isSyncMessage = isSyncMessage
self.totalAttachmentsUploaded = 0
self.snodeMessage = snodeMessage
@ -86,7 +81,6 @@ public final class MessageSender {
self.destination = destination
self.namespace = nil
self.interactionId = interactionId
self.isSyncMessage = false
self.totalAttachmentsUploaded = 0
self.snodeMessage = nil
@ -107,7 +101,6 @@ public final class MessageSender {
self.destination = destination
self.namespace = nil
self.interactionId = interactionId
self.isSyncMessage = false
self.totalAttachmentsUploaded = 0
self.snodeMessage = nil
@ -124,7 +117,6 @@ public final class MessageSender {
destination: destination.with(fileIds: fileIds),
namespace: namespace,
interactionId: interactionId,
isSyncMessage: isSyncMessage,
totalAttachmentsUploaded: fileIds.count,
snodeMessage: snodeMessage,
plaintext: plaintext,
@ -139,7 +131,6 @@ public final class MessageSender {
to destination: Message.Destination,
namespace: SnodeAPI.Namespace?,
interactionId: Int64?,
isSyncMessage: Bool = false,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData {
// Common logic for all destinations
@ -154,7 +145,7 @@ public final class MessageSender {
)
switch destination {
case .contact, .closedGroup:
case .contact, .syncMessage, .closedGroup:
return try prepareSendToSnodeDestination(
db,
message: updatedMessage,
@ -163,7 +154,6 @@ public final class MessageSender {
interactionId: interactionId,
userPublicKey: currentUserPublicKey,
messageSendTimestamp: messageSendTimestamp,
isSyncMessage: isSyncMessage,
using: dependencies
)
@ -198,13 +188,13 @@ public final class MessageSender {
interactionId: Int64?,
userPublicKey: String,
messageSendTimestamp: Int64,
isSyncMessage: Bool = false,
using dependencies: Dependencies
) throws -> PreparedSendData {
message.sender = userPublicKey
message.recipient = {
switch destination {
case .contact(let publicKey): return publicKey
case .syncMessage: return userPublicKey
case .closedGroup(let groupPublicKey): return groupPublicKey
case .openGroup, .openGroupInbox: preconditionFailure()
}
@ -215,6 +205,7 @@ public final class MessageSender {
throw MessageSender.handleFailedMessageSend(
db,
message: message,
destination: destination,
with: .invalidMessage,
interactionId: interactionId,
using: dependencies
@ -223,25 +214,25 @@ public final class MessageSender {
// Attach the user's profile if needed (no need to do so for 'Note to Self' or sync
// messages as they will be managed by the user config handling
let isSelfSend: Bool = (message.recipient == userPublicKey)
if !isSelfSend, !isSyncMessage, var messageWithProfile: MessageWithProfile = message as? MessageWithProfile {
let profile: Profile = Profile.fetchOrCreateCurrentUser(db)
if let profileKey: Data = profile.profileEncryptionKey, let profilePictureUrl: String = profile.profilePictureUrl {
messageWithProfile.profile = VisibleMessage.VMProfile(
displayName: profile.name,
profileKey: profileKey,
profilePictureUrl: profilePictureUrl
)
}
else {
messageWithProfile.profile = VisibleMessage.VMProfile(displayName: profile.name)
}
switch (destination, (message.recipient == userPublicKey), message as? MessageWithProfile) {
case (.syncMessage, _, _), (_, true, _), (_, _, .none): break
case (_, _, .some(var messageWithProfile)):
let profile: Profile = Profile.fetchOrCreateCurrentUser(db)
if let profileKey: Data = profile.profileEncryptionKey, let profilePictureUrl: String = profile.profilePictureUrl {
messageWithProfile.profile = VisibleMessage.VMProfile(
displayName: profile.name,
profileKey: profileKey,
profilePictureUrl: profilePictureUrl
)
}
else {
messageWithProfile.profile = VisibleMessage.VMProfile(displayName: profile.name)
}
}
// Perform any pre-send actions
handleMessageWillSend(db, message: message, interactionId: interactionId, isSyncMessage: isSyncMessage)
handleMessageWillSend(db, message: message, destination: destination, interactionId: interactionId)
// Convert it to protobuf
let threadId: String = Message.threadId(forMessage: message, destination: destination)
@ -250,6 +241,7 @@ public final class MessageSender {
throw MessageSender.handleFailedMessageSend(
db,
message: message,
destination: destination,
with: .protoConversionFailed,
interactionId: interactionId,
using: dependencies
@ -267,6 +259,7 @@ public final class MessageSender {
throw MessageSender.handleFailedMessageSend(
db,
message: message,
destination: destination,
with: .other(error),
interactionId: interactionId,
using: dependencies
@ -280,6 +273,9 @@ public final class MessageSender {
case .contact(let publicKey):
ciphertext = try encryptWithSessionProtocol(db, plaintext: plaintext, for: publicKey, using: dependencies)
case .syncMessage:
ciphertext = try encryptWithSessionProtocol(db, plaintext: plaintext, for: userPublicKey, using: dependencies)
case .closedGroup(let groupPublicKey):
guard let encryptionKeyPair: ClosedGroupKeyPair = try? ClosedGroupKeyPair.fetchLatestKeyPair(db, threadId: groupPublicKey) else {
throw MessageSenderError.noKeyPair
@ -300,6 +296,7 @@ public final class MessageSender {
throw MessageSender.handleFailedMessageSend(
db,
message: message,
destination: destination,
with: .other(error),
interactionId: interactionId,
using: dependencies
@ -311,7 +308,7 @@ public final class MessageSender {
let senderPublicKey: String
switch destination {
case .contact:
case .contact, .syncMessage:
kind = .sessionMessage
senderPublicKey = ""
@ -336,6 +333,7 @@ public final class MessageSender {
throw MessageSender.handleFailedMessageSend(
db,
message: message,
destination: destination,
with: .other(error),
interactionId: interactionId,
using: dependencies
@ -350,13 +348,7 @@ public final class MessageSender {
data: base64EncodedData,
ttl: Message.getSpecifiedTTL(
message: message,
isGroupMessage: {
switch destination {
case .closedGroup: return true
default: return false
}
}(),
isSyncMessage: isSyncMessage
destination: destination
),
timestampMs: UInt64(messageSendTimestamp)
)
@ -366,7 +358,6 @@ public final class MessageSender {
destination: destination,
namespace: namespace,
interactionId: interactionId,
isSyncMessage: isSyncMessage,
snodeMessage: snodeMessage
)
}
@ -382,7 +373,7 @@ public final class MessageSender {
let threadId: String
switch destination {
case .contact, .closedGroup, .openGroupInbox: preconditionFailure()
case .contact, .syncMessage, .closedGroup, .openGroupInbox: preconditionFailure()
case .openGroup(let roomToken, let server, let whisperTo, let whisperMods, _):
threadId = OpenGroup.idFor(roomToken: roomToken, server: server)
message.recipient = [
@ -439,6 +430,7 @@ public final class MessageSender {
throw MessageSender.handleFailedMessageSend(
db,
message: message,
destination: destination,
with: .invalidMessage,
interactionId: interactionId,
using: dependencies
@ -455,6 +447,7 @@ public final class MessageSender {
throw MessageSender.handleFailedMessageSend(
db,
message: message,
destination: destination,
with: .noUsername,
interactionId: interactionId,
using: dependencies
@ -462,13 +455,14 @@ public final class MessageSender {
}
// Perform any pre-send actions
handleMessageWillSend(db, message: message, interactionId: interactionId)
handleMessageWillSend(db, message: message, destination: destination, interactionId: interactionId)
// Convert it to protobuf
guard let proto = message.toProto(db, threadId: threadId) else {
throw MessageSender.handleFailedMessageSend(
db,
message: message,
destination: destination,
with: .protoConversionFailed,
interactionId: interactionId,
using: dependencies
@ -486,6 +480,7 @@ public final class MessageSender {
throw MessageSender.handleFailedMessageSend(
db,
message: message,
destination: destination,
with: .other(error),
interactionId: interactionId,
using: dependencies
@ -533,13 +528,14 @@ public final class MessageSender {
}
// Perform any pre-send actions
handleMessageWillSend(db, message: message, interactionId: interactionId)
handleMessageWillSend(db, message: message, destination: destination, interactionId: interactionId)
// Convert it to protobuf
guard let proto = message.toProto(db, threadId: recipientBlindedPublicKey) else {
throw MessageSender.handleFailedMessageSend(
db,
message: message,
destination: destination,
with: .protoConversionFailed,
interactionId: interactionId,
using: dependencies
@ -557,6 +553,7 @@ public final class MessageSender {
throw MessageSender.handleFailedMessageSend(
db,
message: message,
destination: destination,
with: .other(error),
interactionId: interactionId,
using: dependencies
@ -580,6 +577,7 @@ public final class MessageSender {
throw MessageSender.handleFailedMessageSend(
db,
message: message,
destination: destination,
with: .other(error),
interactionId: interactionId,
using: dependencies
@ -628,9 +626,9 @@ public final class MessageSender {
MessageSender.handleFailedMessageSend(
db,
message: message,
destination: data.destination,
with: .attachmentsNotUploaded,
interactionId: data.interactionId,
isSyncMessage: (data.isSyncMessage == true),
using: dependencies
)
}
@ -646,7 +644,7 @@ public final class MessageSender {
}
switch data.destination {
case .contact, .closedGroup: return sendToSnodeDestination(data: data, using: dependencies)
case .contact, .syncMessage, .closedGroup: return sendToSnodeDestination(data: data, using: dependencies)
case .openGroup: return sendToOpenGroupDestination(data: data, using: dependencies)
case .openGroupInbox: return sendToOpenGroupInbox(data: data, using: dependencies)
}
@ -661,7 +659,6 @@ public final class MessageSender {
guard
let message: Message = data.message,
let namespace: SnodeAPI.Namespace = data.namespace,
let isSyncMessage: Bool = data.isSyncMessage,
let snodeMessage: SnodeMessage = data.snodeMessage
else {
return Fail(error: MessageSenderError.invalidMessage)
@ -680,9 +677,10 @@ public final class MessageSender {
details: NotifyPushServerJob.Details(message: snodeMessage)
)
let shouldNotify: Bool = {
switch updatedMessage {
case is VisibleMessage, is UnsendRequest: return !isSyncMessage
case let callMessage as CallMessage:
switch (updatedMessage, data.destination) {
case (is VisibleMessage, .syncMessage), (is UnsendRequest, .syncMessage): return false
case (is VisibleMessage, _), (is UnsendRequest, _): return true
case (let callMessage as CallMessage, _):
// Note: Other 'CallMessage' types are too big to send as push notifications
// so only send the 'preOffer' message as a notification
switch callMessage.kind {
@ -701,7 +699,6 @@ public final class MessageSender {
message: updatedMessage,
to: data.destination,
interactionId: data.interactionId,
isSyncMessage: isSyncMessage,
using: dependencies
)
@ -758,6 +755,7 @@ public final class MessageSender {
MessageSender.handleFailedMessageSend(
db,
message: message,
destination: data.destination,
with: .other(error),
interactionId: data.interactionId,
using: dependencies
@ -829,6 +827,7 @@ public final class MessageSender {
MessageSender.handleFailedMessageSend(
db,
message: message,
destination: data.destination,
with: .other(error),
interactionId: data.interactionId,
using: dependencies
@ -893,6 +892,7 @@ public final class MessageSender {
MessageSender.handleFailedMessageSend(
db,
message: message,
destination: data.destination,
with: .other(error),
interactionId: data.interactionId,
using: dependencies
@ -909,27 +909,33 @@ public final class MessageSender {
public static func handleMessageWillSend(
_ db: Database,
message: Message,
interactionId: Int64?,
isSyncMessage: Bool = false
destination: Message.Destination,
interactionId: Int64?
) {
// If the message was a reaction then we don't want to do anything to the original
// interaction (which the 'interactionId' is pointing to
guard (message as? VisibleMessage)?.reaction == nil else { return }
// Mark messages as "sending"/"syncing" if needed (this is for retries)
_ = try? RecipientState
.filter(RecipientState.Columns.interactionId == interactionId)
.filter(isSyncMessage ?
RecipientState.Columns.state == RecipientState.State.failedToSync :
RecipientState.Columns.state == RecipientState.State.failed
)
.updateAll(
db,
RecipientState.Columns.state.set(to: isSyncMessage ?
RecipientState.State.syncing :
RecipientState.State.sending
)
)
switch destination {
case .syncMessage:
_ = try? RecipientState
.filter(RecipientState.Columns.interactionId == interactionId)
.filter(RecipientState.Columns.state == RecipientState.State.failedToSync)
.updateAll(
db,
RecipientState.Columns.state.set(to: RecipientState.State.syncing)
)
default:
_ = try? RecipientState
.filter(RecipientState.Columns.interactionId == interactionId)
.filter(RecipientState.Columns.state == RecipientState.State.failed)
.updateAll(
db,
RecipientState.Columns.state.set(to: RecipientState.State.sending)
)
}
}
private static func handleSuccessfulMessageSend(
@ -938,7 +944,6 @@ public final class MessageSender {
to destination: Message.Destination,
interactionId: Int64?,
serverTimestampMs: UInt64? = nil,
isSyncMessage: Bool = false,
using dependencies: Dependencies
) throws {
// If the message was a reaction then we want to update the reaction instead of the original
@ -957,59 +962,61 @@ public final class MessageSender {
// Get the visible message if possible
if let interaction: Interaction = interaction {
// Only store the server hash of a sync message if the message is self send valid
if (message.isSelfSendValid && isSyncMessage || !isSyncMessage) {
try interaction.with(
serverHash: message.serverHash,
// Track the open group server message ID and update server timestamp (use server
// timestamp for open group messages otherwise the quote messages may not be able
// to be found by the timestamp on other devices
timestampMs: (message.openGroupServerMessageId == nil ?
nil :
serverTimestampMs.map { Int64($0) }
),
openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) }
).update(db)
if interaction.isExpiringMessage {
// Start disappearing messages job after a message is successfully sent.
// For DAR and DAS outgoing messages, the expiration start time are the
// same as message sentTimestamp. So do this once, DAR and DAS messages
// should all be covered.
dependencies.jobRunner.upsert(
db,
job: DisappearingMessagesJob.updateNextRunIfNeeded(
db,
interaction: interaction,
startedAtMs: Double(interaction.timestampMs)
switch (message.isSelfSendValid, destination) {
case (false, .syncMessage): break
case (true, .syncMessage), (_, .contact), (_, .closedGroup), (_, .openGroup), (_, .openGroupInbox):
try interaction.with(
serverHash: message.serverHash,
// Track the open group server message ID and update server timestamp (use server
// timestamp for open group messages otherwise the quote messages may not be able
// to be found by the timestamp on other devices
timestampMs: (message.openGroupServerMessageId == nil ?
nil :
serverTimestampMs.map { Int64($0) }
),
canStartJob: true,
using: dependencies
)
openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) }
).update(db)
if
isSyncMessage,
let startedAtMs: Double = interaction.expiresStartedAtMs,
let expiresInSeconds: TimeInterval = interaction.expiresInSeconds,
let serverHash: String = message.serverHash
{
let expirationTimestampMs: Int64 = Int64(startedAtMs + expiresInSeconds * 1000)
dependencies.jobRunner.add(
if interaction.isExpiringMessage {
// Start disappearing messages job after a message is successfully sent.
// For DAR and DAS outgoing messages, the expiration start time are the
// same as message sentTimestamp. So do this once, DAR and DAS messages
// should all be covered.
dependencies.jobRunner.upsert(
db,
job: Job(
variant: .expirationUpdate,
behaviour: .runOnce,
threadId: interaction.threadId,
details: ExpirationUpdateJob.Details(
serverHashes: [serverHash],
expirationTimestampMs: expirationTimestampMs
)
job: DisappearingMessagesJob.updateNextRunIfNeeded(
db,
interaction: interaction,
startedAtMs: Double(interaction.timestampMs)
),
canStartJob: true,
using: dependencies
)
if
case .syncMessage = destination,
let startedAtMs: Double = interaction.expiresStartedAtMs,
let expiresInSeconds: TimeInterval = interaction.expiresInSeconds,
let serverHash: String = message.serverHash
{
let expirationTimestampMs: Int64 = Int64(startedAtMs + expiresInSeconds * 1000)
dependencies.jobRunner.add(
db,
job: Job(
variant: .expirationUpdate,
behaviour: .runOnce,
threadId: interaction.threadId,
details: ExpirationUpdateJob.Details(
serverHashes: [serverHash],
expirationTimestampMs: expirationTimestampMs
)
),
canStartJob: true,
using: dependencies
)
}
}
}
}
// Mark the message as sent
try interaction.recipientStates
@ -1038,7 +1045,6 @@ public final class MessageSender {
destination: destination,
threadId: threadId,
interactionId: interactionId,
isAlreadySyncMessage: isSyncMessage,
using: dependencies
)
}
@ -1046,9 +1052,9 @@ public final class MessageSender {
@discardableResult internal static func handleFailedMessageSend(
_ db: Database,
message: Message,
destination: Message.Destination?,
with error: MessageSenderError,
interactionId: Int64?,
isSyncMessage: Bool = false,
using dependencies: Dependencies
) -> Error {
// If the message was a reaction then we don't want to do anything to the original
@ -1060,18 +1066,27 @@ public final class MessageSender {
// Note: The 'db' could be either read-only or writeable so we determine
// if a change is required, and if so dispatch to a separate queue for the
// actual write
let rowIds: [Int64] = (try? RecipientState
.select(Column.rowID)
.filter(RecipientState.Columns.interactionId == interactionId)
.filter(!isSyncMessage ?
RecipientState.Columns.state == RecipientState.State.sending : (
RecipientState.Columns.state == RecipientState.State.syncing ||
RecipientState.Columns.state == RecipientState.State.sent
)
)
.asRequest(of: Int64.self)
.fetchAll(db))
.defaulting(to: [])
let rowIds: [Int64] = (try? {
switch destination {
case .syncMessage:
return RecipientState
.select(Column.rowID)
.filter(RecipientState.Columns.interactionId == interactionId)
.filter(
RecipientState.Columns.state == RecipientState.State.syncing ||
RecipientState.Columns.state == RecipientState.State.sent
)
default:
return RecipientState
.select(Column.rowID)
.filter(RecipientState.Columns.interactionId == interactionId)
.filter(RecipientState.Columns.state == RecipientState.State.sending)
}
}()
.asRequest(of: Int64.self)
.fetchAll(db))
.defaulting(to: [])
guard !rowIds.isEmpty else { return error }
@ -1079,15 +1094,25 @@ public final class MessageSender {
// issue from occuring in some cases
DispatchQueue.global(qos: .background).async {
dependencies.storage.write { db in
try RecipientState
.filter(rowIds.contains(Column.rowID))
.updateAll(
db,
RecipientState.Columns.state.set(
to: (isSyncMessage ? RecipientState.State.failedToSync : RecipientState.State.failed)
),
RecipientState.Columns.mostRecentFailureText.set(to: error.localizedDescription)
)
switch destination {
case .syncMessage:
try RecipientState
.filter(rowIds.contains(Column.rowID))
.updateAll(
db,
RecipientState.Columns.state.set(to: RecipientState.State.failedToSync),
RecipientState.Columns.mostRecentFailureText.set(to: "\(error)")
)
default:
try RecipientState
.filter(rowIds.contains(Column.rowID))
.updateAll(
db,
RecipientState.Columns.state.set(to: RecipientState.State.failed),
RecipientState.Columns.mostRecentFailureText.set(to: "\(error)")
)
}
}
}
@ -1116,7 +1141,6 @@ public final class MessageSender {
destination: Message.Destination,
threadId: String?,
interactionId: Int64?,
isAlreadySyncMessage: Bool,
using dependencies: Dependencies
) {
// Sync the message if it's not a sync message, wasn't already sent to the current user and
@ -1125,7 +1149,6 @@ public final class MessageSender {
if
case .contact(let publicKey) = destination,
!isAlreadySyncMessage,
publicKey != currentUserPublicKey,
Message.shouldSync(message: message)
{
@ -1139,9 +1162,8 @@ public final class MessageSender {
threadId: threadId,
interactionId: interactionId,
details: MessageSendJob.Details(
destination: .contact(publicKey: currentUserPublicKey),
message: message,
isSyncMessage: true
destination: .syncMessage(originalRecipientPublicKey: publicKey),
message: message
)
),
canStartJob: true,

@ -253,7 +253,9 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
}(),
using: dependencies
)
.tryFlatMapWithRandomSnode { SnodeAPI.getNetworkTime(from: $0, using: dependencies) }
.tryFlatMapWithRandomSnode(using: dependencies) {
SnodeAPI.getNetworkTime(from: $0, using: dependencies)
}
.map { _ in () }
.eraseToAnyPublisher()
}

@ -0,0 +1,3 @@
// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
import Foundation

@ -50,12 +50,12 @@ extension SnodeAPI {
memLimit: sodium.pwHash.MemLimitModerate,
alg: .Argon2ID13
)
else { throw SnodeAPIError.hashingFailed }
else { throw SnodeAPIError.onsHashingFailed }
let nonce: [UInt8] = Data(repeating: 0, count: sodium.secretBox.NonceBytes).bytes
guard let sessionIdAsData: [UInt8] = sodium.secretBox.open(authenticatedCipherText: ciphertext, secretKey: key, nonce: nonce) else {
throw SnodeAPIError.decryptionFailed
throw SnodeAPIError.onsDecryptionFailed
}
return sessionIdAsData.toHexString()
@ -66,7 +66,7 @@ extension SnodeAPI {
// xchacha-based encryption
// key = H(name, key=H(name))
guard let key: [UInt8] = sodium.genericHash.hash(message: nameBytes, key: nameHashBytes) else {
throw SnodeAPIError.hashingFailed
throw SnodeAPIError.onsHashingFailed
}
guard
// Should always be equal in practice
@ -76,7 +76,7 @@ extension SnodeAPI {
secretKey: key,
nonce: nonceBytes
)
else { throw SnodeAPIError.decryptionFailed }
else { throw SnodeAPIError.onsDecryptionFailed }
return sessionIdAsData.toHexString()
}

@ -600,7 +600,7 @@ public final class SnodeAPI {
let nameAsData = [UInt8](onsName.data(using: String.Encoding.utf8)!)
guard let nameHash = sodium.wrappedValue.genericHash.hash(message: nameAsData) else {
return Fail(error: SnodeAPIError.hashingFailed)
return Fail(error: SnodeAPIError.onsHashingFailed)
.eraseToAnyPublisher()
}
@ -647,7 +647,7 @@ public final class SnodeAPI {
.collect()
.tryMap { results -> String in
guard results.count == validationCount, Set(results).count == 1 else {
throw SnodeAPIError.validationFailed
throw SnodeAPIError.onsValidationFailed
}
return results[0]
@ -1362,6 +1362,7 @@ public extension Publisher where Output == Set<Snode> {
return swarm.subtracting(usedSnodes)
}
}()
var lastError: Error?
return Just(())
.setFailureType(to: Error.self)
@ -1374,7 +1375,7 @@ public extension Publisher where Output == Set<Snode> {
// Select the next snode
return try dependencies.popRandomElement(&remainingSnodes) ?? {
throw SnodeAPIError.ranOutOfRandomSnodes
throw SnodeAPIError.ranOutOfRandomSnodes(lastError)
}()
}()
drainBehaviour.mutate { $0 = $0.use(snode: snode, from: swarm) }
@ -1382,33 +1383,14 @@ public extension Publisher where Output == Set<Snode> {
return try transform(snode)
.eraseToAnyPublisher()
}
.retry(retries)
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
// MARK: - Convenience
public extension Publisher where Output == Set<Snode> {
func tryFlatMapWithRandomSnode<T, P>(
maxPublishers: Subscribers.Demand = .unlimited,
retry retries: Int = 0,
_ transform: @escaping (Snode) throws -> P
) -> AnyPublisher<T, Error> where T == P.Output, P: Publisher, P.Failure == Error {
return self
.mapError { $0 }
.flatMap(maxPublishers: maxPublishers) { swarm -> AnyPublisher<T, Error> in
var remainingSnodes: Set<Snode> = swarm
return Just(())
.setFailureType(to: Error.self)
.tryFlatMap(maxPublishers: maxPublishers) { _ -> AnyPublisher<T, Error> in
let snode: Snode = try remainingSnodes.popRandomElement() ?? { throw SnodeAPIError.ranOutOfRandomSnodes }()
.mapError { error in
// Prevent nesting the 'ranOutOfRandomSnodes' errors
switch error {
case SnodeAPIError.ranOutOfRandomSnodes: break
default: lastError = error
}
return try transform(snode)
.eraseToAnyPublisher()
return error
}
.retry(retries)
.eraseToAnyPublisher()

@ -21,12 +21,12 @@ public enum SnodeAPIError: Error, CustomStringConvertible {
// Onion Request Errors
case emptySnodePool
case insufficientSnodes
case ranOutOfRandomSnodes
case ranOutOfRandomSnodes(Error?)
// ONS
case decryptionFailed
case hashingFailed
case validationFailed
case onsDecryptionFailed
case onsHashingFailed
case onsValidationFailed
// Quic
case invalidPayload
@ -51,12 +51,18 @@ public enum SnodeAPIError: Error, CustomStringConvertible {
// Onion Request Errors
case .emptySnodePool: return "Service Node pool is empty (SnodeAPIError.emptySnodePool)."
case .insufficientSnodes: return "Couldn't find enough Service Nodes to build a path (SnodeAPIError.insufficientSnodes)."
case .ranOutOfRandomSnodes: return "Ran out of random snodes to send the request through (SnodeAPIError.ranOutOfRandomSnodes)."
case .ranOutOfRandomSnodes(let maybeError):
switch maybeError {
case .none: return "Ran out of random snodes (SnodeAPIError.ranOutOfRandomSnodes(nil))."
case .some(let error):
let errorDesc = "\(error)".trimmingCharacters(in: CharacterSet(["."]))
return "Ran out of random snodes (SnodeAPIError.ranOutOfRandomSnodes(\(errorDesc))."
}
// ONS
case .decryptionFailed: return "Couldn't decrypt ONS name (SnodeAPIError.decryptionFailed)."
case .hashingFailed: return "Couldn't compute ONS name hash (SnodeAPIError.hashingFailed)."
case .validationFailed: return "ONS name validation failed (SnodeAPIError.validationFailed)."
case .onsDecryptionFailed: return "Couldn't decrypt ONS name (SnodeAPIError.onsDecryptionFailed)."
case .onsHashingFailed: return "Couldn't compute ONS name hash (SnodeAPIError.onsHashingFailed)."
case .onsValidationFailed: return "ONS name validation failed (SnodeAPIError.onsValidationFailed)."
// Quic
case .invalidPayload: return "Invalid payload (SnodeAPIError.invalidPayload)."

@ -0,0 +1,3 @@
// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
import Foundation

@ -1,35 +0,0 @@
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
import Foundation
import SignalCoreKit
extension UIAlertController {
@objc
public func applyAccessibilityIdentifiers() {
for action in actions {
guard let view = action.value(forKey: "__representer") as? UIView else {
owsFailDebug("Missing representer.")
continue
}
view.accessibilityIdentifier = action.accessibilityIdentifier
}
}
}
// MARK: -
extension UIAlertAction {
private struct AssociatedKeys {
static var AccessibilityIdentifier = "ows_accessibilityIdentifier"
}
@objc
public var accessibilityIdentifier: String? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.AccessibilityIdentifier) as? String
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.AccessibilityIdentifier, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
}
Loading…
Cancel
Save