mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
f9c2655df4
Fixed an issue where messages where signed incorrectly when blinding wasn't enabled on a SOGS Fixed an issue where a single invalid message would result in all messages in that request being dropped Updated the final legacy endpoint (ban and delete all messages) Moved the OpenGroupManager poller values into the 'Cache' (so they are thread safe) Started adding unit tests for the OpenGroupManager Removed some redundant parameters from the 'Request' type
563 lines
27 KiB
Swift
563 lines
27 KiB
Swift
import PromiseKit
|
|
import SessionSnodeKit
|
|
import SessionUtilitiesKit
|
|
import Sodium
|
|
|
|
@objc(SNMessageSender)
|
|
public final class MessageSender : NSObject {
|
|
|
|
// MARK: Error
|
|
public enum Error : LocalizedError {
|
|
case invalidMessage
|
|
case protoConversionFailed
|
|
case noUserX25519KeyPair
|
|
case noUserED25519KeyPair
|
|
case signingFailed
|
|
case encryptionFailed
|
|
case noUsername
|
|
// Closed groups
|
|
case noThread
|
|
case noKeyPair
|
|
case invalidClosedGroupUpdate
|
|
|
|
internal var isRetryable: Bool {
|
|
switch self {
|
|
case .invalidMessage, .protoConversionFailed, .invalidClosedGroupUpdate, .signingFailed, .encryptionFailed: return false
|
|
default: return true
|
|
}
|
|
}
|
|
|
|
public var errorDescription: 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."
|
|
// 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."
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Initialization
|
|
private override init() { }
|
|
|
|
// MARK: Preparation
|
|
public static func prep(_ signalAttachments: [SignalAttachment], for message: VisibleMessage, using transaction: YapDatabaseReadWriteTransaction) {
|
|
guard let tsMessage = TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) else {
|
|
#if DEBUG
|
|
preconditionFailure()
|
|
#else
|
|
return
|
|
#endif
|
|
}
|
|
var attachments: [TSAttachmentStream] = []
|
|
signalAttachments.forEach {
|
|
let attachment = TSAttachmentStream(contentType: $0.mimeType, byteCount: UInt32($0.dataLength), sourceFilename: $0.sourceFilename,
|
|
caption: $0.captionText, albumMessageId: tsMessage.uniqueId!)
|
|
attachment.attachmentType = $0.isVoiceMessage ? .voiceMessage : .default
|
|
attachments.append(attachment)
|
|
attachment.write($0.dataSource)
|
|
attachment.save(with: transaction)
|
|
}
|
|
prep(attachments, for: message, using: transaction)
|
|
}
|
|
|
|
@objc(prep:forMessage:usingTransaction:)
|
|
public static func prep(_ attachmentStreams: [TSAttachmentStream], for message: VisibleMessage, using transaction: YapDatabaseReadWriteTransaction) {
|
|
guard let tsMessage = TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) else {
|
|
#if DEBUG
|
|
preconditionFailure()
|
|
#else
|
|
return
|
|
#endif
|
|
}
|
|
var attachments = attachmentStreams
|
|
// The line below locally generates a thumbnail for the quoted attachment. It just needs to happen at some point during the
|
|
// message sending process.
|
|
tsMessage.quotedMessage?.createThumbnailAttachmentsIfNecessary(with: transaction)
|
|
var linkPreviewAttachmentID: String?
|
|
if let id = tsMessage.linkPreview?.imageAttachmentId,
|
|
let attachment = TSAttachment.fetch(uniqueId: id, transaction: transaction) as? TSAttachmentStream {
|
|
linkPreviewAttachmentID = id
|
|
attachments.append(attachment)
|
|
}
|
|
// Anything added to message.attachmentIDs will be uploaded by an UploadAttachmentJob. Any attachment IDs added to tsMessage will
|
|
// make it render as an attachment (not what we want in the case of a link preview or quoted attachment).
|
|
message.attachmentIDs = attachments.map { $0.uniqueId! }
|
|
tsMessage.attachmentIds.removeAllObjects()
|
|
tsMessage.attachmentIds.addObjects(from: message.attachmentIDs)
|
|
if let id = linkPreviewAttachmentID { tsMessage.attachmentIds.remove(id) }
|
|
tsMessage.save(with: transaction)
|
|
}
|
|
|
|
// MARK: Convenience
|
|
public static func send(_ message: Message, to destination: Message.Destination, using transaction: Any) -> Promise<Void> {
|
|
switch destination {
|
|
case .contact, .closedGroup:
|
|
return sendToSnodeDestination(destination, message: message, using: transaction)
|
|
|
|
case .legacyOpenGroup, .openGroup:
|
|
return sendToOpenGroupDestination(destination, message: message, using: transaction)
|
|
|
|
case .openGroupInbox:
|
|
return sendToOpenGroupInboxDestination(destination, message: message, using: transaction)
|
|
}
|
|
}
|
|
|
|
// MARK: One-on-One Chats & Closed Groups
|
|
internal static func sendToSnodeDestination(_ destination: Message.Destination, message: Message, using transaction: Any, isSyncMessage: Bool = false) -> Promise<Void> {
|
|
let (promise, seal) = Promise<Void>.pending()
|
|
let storage = SNMessagingKitConfiguration.shared.storage
|
|
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
|
let userPublicKey = storage.getUserPublicKey()
|
|
var isMainAppAndActive = false
|
|
if let sharedUserDefaults = UserDefaults(suiteName: "group.com.loki-project.loki-messenger") {
|
|
isMainAppAndActive = sharedUserDefaults.bool(forKey: "isMainAppActive")
|
|
}
|
|
// Set the timestamp, sender and recipient
|
|
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set
|
|
message.sentTimestamp = NSDate.millisecondTimestamp()
|
|
}
|
|
message.sender = userPublicKey
|
|
|
|
switch destination {
|
|
case .contact(let publicKey): message.recipient = publicKey
|
|
case .closedGroup(let groupPublicKey): message.recipient = groupPublicKey
|
|
case .legacyOpenGroup, .openGroup, .openGroupInbox: preconditionFailure()
|
|
}
|
|
|
|
let isSelfSend = (message.recipient == userPublicKey)
|
|
// Set the failure handler (need it here already for precondition failure handling)
|
|
func handleFailure(with error: Swift.Error, using transaction: YapDatabaseReadWriteTransaction) {
|
|
MessageSender.handleFailedMessageSend(message, with: error, using: transaction)
|
|
seal.reject(error)
|
|
}
|
|
// Validate the message
|
|
guard message.isValid else { handleFailure(with: Error.invalidMessage, using: transaction); return promise }
|
|
// Stop here if this is a self-send, unless it's:
|
|
// • a configuration message
|
|
// • a sync message
|
|
// • a closed group control message of type `new`
|
|
// • an unsend request
|
|
let isNewClosedGroupControlMessage = given(message as? ClosedGroupControlMessage) { if case .new = $0.kind { return true } else { return false } } ?? false
|
|
guard !isSelfSend || message is ConfigurationMessage || isSyncMessage || isNewClosedGroupControlMessage || message is UnsendRequest else {
|
|
storage.write(with: { transaction in
|
|
MessageSender.handleSuccessfulMessageSend(message, to: destination, using: transaction)
|
|
seal.fulfill(())
|
|
}, completion: { })
|
|
return promise
|
|
}
|
|
// Attach the user's profile if needed
|
|
if let message = message as? VisibleMessage {
|
|
guard let name = storage.getUser()?.name else { handleFailure(with: Error.noUsername, using: transaction); return promise }
|
|
if let profileKey = storage.getUser()?.profileEncryptionKey?.keyData, let profilePictureURL = storage.getUser()?.profilePictureURL {
|
|
message.profile = VisibleMessage.Profile(displayName: name, profileKey: profileKey, profilePictureURL: profilePictureURL)
|
|
} else {
|
|
message.profile = VisibleMessage.Profile(displayName: name)
|
|
}
|
|
}
|
|
// Convert it to protobuf
|
|
guard let proto = message.toProto(using: transaction) else { handleFailure(with: Error.protoConversionFailed, using: transaction); return promise }
|
|
// Serialize the protobuf
|
|
let plaintext: Data
|
|
do {
|
|
plaintext = (try proto.serializedData() as NSData).paddedMessageBody()
|
|
} catch {
|
|
SNLog("Couldn't serialize proto due to error: \(error).")
|
|
handleFailure(with: error, using: transaction)
|
|
return promise
|
|
}
|
|
// Encrypt the serialized protobuf
|
|
let ciphertext: Data
|
|
do {
|
|
switch destination {
|
|
case .contact(let publicKey):
|
|
ciphertext = try encryptWithSessionProtocol(plaintext, for: publicKey)
|
|
|
|
case .closedGroup(let groupPublicKey):
|
|
guard let encryptionKeyPair = Storage.shared.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else {
|
|
throw Error.noKeyPair
|
|
}
|
|
|
|
ciphertext = try encryptWithSessionProtocol(plaintext, for: encryptionKeyPair.hexEncodedPublicKey)
|
|
|
|
case .legacyOpenGroup, .openGroup, .openGroupInbox: preconditionFailure()
|
|
}
|
|
}
|
|
catch {
|
|
SNLog("Couldn't encrypt message for destination: \(destination) due to error: \(error).")
|
|
handleFailure(with: error, using: transaction)
|
|
return promise
|
|
}
|
|
|
|
// Wrap the result
|
|
let kind: SNProtoEnvelope.SNProtoEnvelopeType
|
|
let senderPublicKey: String
|
|
|
|
switch destination {
|
|
case .contact:
|
|
kind = .sessionMessage
|
|
senderPublicKey = ""
|
|
|
|
case .closedGroup(let groupPublicKey):
|
|
kind = .closedGroupMessage
|
|
senderPublicKey = groupPublicKey
|
|
|
|
case .legacyOpenGroup, .openGroup, .openGroupInbox: preconditionFailure()
|
|
}
|
|
|
|
let wrappedMessage: Data
|
|
do {
|
|
wrappedMessage = try MessageWrapper.wrap(type: kind, timestamp: message.sentTimestamp!,
|
|
senderPublicKey: senderPublicKey, base64EncodedContent: ciphertext.base64EncodedString())
|
|
} catch {
|
|
SNLog("Couldn't wrap message due to error: \(error).")
|
|
handleFailure(with: error, using: transaction)
|
|
return promise
|
|
}
|
|
|
|
// Send the result
|
|
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
|
|
var isSuccess = false
|
|
let promiseCount = promises.count
|
|
var errorCount = 0
|
|
promises.forEach {
|
|
let _ = $0.done(on: DispatchQueue.global(qos: .userInitiated)) { rawResponse in
|
|
guard !isSuccess else { return } // Succeed as soon as the first promise succeeds
|
|
isSuccess = true
|
|
storage.write(with: { transaction in
|
|
let json = rawResponse as? JSON
|
|
let hash = json?["hash"] as? String
|
|
message.serverHash = hash
|
|
MessageSender.handleSuccessfulMessageSend(message, to: destination, isSyncMessage: isSyncMessage, using: transaction)
|
|
var shouldNotify = ((message is VisibleMessage || message is UnsendRequest) && !isSyncMessage)
|
|
/*
|
|
if let closedGroupControlMessage = message as? ClosedGroupControlMessage, case .new = closedGroupControlMessage.kind {
|
|
shouldNotify = true
|
|
}
|
|
*/
|
|
if shouldNotify {
|
|
let notifyPNServerJob = NotifyPNServerJob(message: snodeMessage)
|
|
if isMainAppAndActive {
|
|
JobQueue.shared.add(notifyPNServerJob, using: transaction)
|
|
seal.fulfill(())
|
|
} else {
|
|
notifyPNServerJob.execute().done(on: DispatchQueue.global(qos: .userInitiated)) {
|
|
seal.fulfill(())
|
|
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { _ in
|
|
seal.fulfill(()) // Always fulfill because the notify PN server job isn't critical.
|
|
}
|
|
}
|
|
} else {
|
|
seal.fulfill(())
|
|
}
|
|
}, completion: { })
|
|
}
|
|
$0.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
|
errorCount += 1
|
|
guard errorCount == promiseCount else { return } // Only error out if all promises failed
|
|
storage.write(with: { transaction in
|
|
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
|
|
}, completion: { })
|
|
}
|
|
}
|
|
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
|
SNLog("Couldn't send message due to error: \(error).")
|
|
storage.write(with: { transaction in
|
|
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
|
|
}, completion: { })
|
|
}
|
|
// Return
|
|
return promise
|
|
}
|
|
|
|
// MARK: - Open Groups
|
|
|
|
internal static func sendToOpenGroupDestination(_ destination: Message.Destination, message: Message, using transaction: Any, dependencies: Dependencies = Dependencies()) -> Promise<Void> {
|
|
let (promise, seal) = Promise<Void>.pending()
|
|
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
|
|
|
// Set the timestamp, sender and recipient
|
|
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set
|
|
message.sentTimestamp = UInt64(dependencies.date.timeIntervalSince1970 * 1000) // Should be in ms
|
|
}
|
|
|
|
guard let threadId: String = message.threadID, let openGroup = dependencies.storage.getOpenGroup(for: threadId) else {
|
|
preconditionFailure()
|
|
}
|
|
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { preconditionFailure() }
|
|
|
|
let server: OpenGroupAPI.Server? = dependencies.storage.getOpenGroupServer(name: openGroup.server)
|
|
|
|
if server?.capabilities.capabilities.contains(.blind) == true {
|
|
guard let serverPublicKey = dependencies.storage.getOpenGroupPublicKey(for: openGroup.server) else {
|
|
preconditionFailure()
|
|
}
|
|
guard let blindedKeyPair: Box.KeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
|
|
preconditionFailure()
|
|
}
|
|
|
|
message.sender = SessionId(.blinded, publicKey: blindedKeyPair.publicKey).hexString
|
|
}
|
|
else {
|
|
message.sender = SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString
|
|
}
|
|
|
|
switch destination {
|
|
case .contact, .closedGroup, .openGroupInbox: preconditionFailure()
|
|
case .legacyOpenGroup(let channel, let server): message.recipient = "\(server).\(channel)"
|
|
|
|
case .openGroup(let room, let server, let whisperTo, let whisperMods, _):
|
|
message.recipient = [
|
|
server,
|
|
room,
|
|
whisperTo,
|
|
(whisperTo == nil && whisperMods ? "mods" : nil)
|
|
]
|
|
.compactMap { $0 }
|
|
.joined(separator: ".")
|
|
}
|
|
|
|
// Set the failure handler (need it here already for precondition failure handling)
|
|
func handleFailure(with error: Swift.Error, using transaction: YapDatabaseReadWriteTransaction) {
|
|
MessageSender.handleFailedMessageSend(message, with: error, using: transaction)
|
|
seal.reject(error)
|
|
}
|
|
|
|
// Validate the message
|
|
guard let message = message as? VisibleMessage else {
|
|
#if DEBUG
|
|
preconditionFailure()
|
|
#else
|
|
handleFailure(with: Error.invalidMessage, using: transaction)
|
|
return promise
|
|
#endif
|
|
}
|
|
guard message.isValid else {
|
|
handleFailure(with: Error.invalidMessage, using: transaction)
|
|
return promise
|
|
}
|
|
|
|
// Attach the user's profile
|
|
guard let name = dependencies.storage.getUser()?.name else {
|
|
handleFailure(with: Error.noUsername, using: transaction)
|
|
return promise
|
|
}
|
|
|
|
if let profileKey = dependencies.storage.getUser()?.profileEncryptionKey?.keyData, let profilePictureURL = dependencies.storage.getUser()?.profilePictureURL {
|
|
message.profile = VisibleMessage.Profile(displayName: name, profileKey: profileKey, profilePictureURL: profilePictureURL)
|
|
}
|
|
else {
|
|
message.profile = VisibleMessage.Profile(displayName: name)
|
|
}
|
|
|
|
// Convert it to protobuf
|
|
guard let proto = message.toProto(using: transaction) else {
|
|
handleFailure(with: Error.protoConversionFailed, using: transaction)
|
|
return promise
|
|
}
|
|
|
|
// Serialize the protobuf
|
|
let plaintext: Data
|
|
|
|
do {
|
|
plaintext = (try proto.serializedData() as NSData).paddedMessageBody()
|
|
}
|
|
catch {
|
|
SNLog("Couldn't serialize proto due to error: \(error).")
|
|
handleFailure(with: error, using: transaction)
|
|
return promise
|
|
}
|
|
|
|
// Send the result
|
|
|
|
guard case .openGroup(let room, let server, let whisperTo, let whisperMods, let fileIds) = destination else {
|
|
preconditionFailure()
|
|
}
|
|
|
|
OpenGroupAPI
|
|
.send(
|
|
plaintext,
|
|
to: room,
|
|
on: server,
|
|
whisperTo: whisperTo,
|
|
whisperMods: whisperMods,
|
|
fileIds: fileIds,
|
|
using: dependencies
|
|
)
|
|
.done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in
|
|
message.openGroupServerMessageID = UInt64(data.id)
|
|
|
|
dependencies.storage.write { transaction in
|
|
// The `posted` value is in seconds but we sent it in ms so need that for de-duping
|
|
MessageSender.handleSuccessfulMessageSend(message, to: destination, serverTimestamp: UInt64(floor(data.posted * 1000)), using: transaction)
|
|
seal.fulfill(())
|
|
}
|
|
}
|
|
.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
|
dependencies.storage.write { transaction in
|
|
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
|
|
}
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
internal static func sendToOpenGroupInboxDestination(_ destination: Message.Destination, message: Message, using transaction: Any, dependencies: Dependencies = Dependencies()) -> Promise<Void> {
|
|
let (promise, seal) = Promise<Void>.pending()
|
|
let storage = SNMessagingKitConfiguration.shared.storage
|
|
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
|
let userPublicKey = storage.getUserPublicKey()
|
|
|
|
guard case .openGroupInbox(let server, let openGroupPublicKey, let recipientBlindedPublicKey) = destination else {
|
|
preconditionFailure()
|
|
}
|
|
|
|
// Set the timestamp, sender and recipient
|
|
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set
|
|
message.sentTimestamp = NSDate.millisecondTimestamp()
|
|
}
|
|
|
|
message.sender = userPublicKey
|
|
message.recipient = recipientBlindedPublicKey
|
|
|
|
// Set the failure handler (need it here already for precondition failure handling)
|
|
func handleFailure(with error: Swift.Error, using transaction: YapDatabaseReadWriteTransaction) {
|
|
MessageSender.handleFailedMessageSend(message, with: error, using: transaction)
|
|
seal.reject(error)
|
|
}
|
|
|
|
// Attach the user's profile if needed
|
|
if let message = message as? VisibleMessage {
|
|
guard let name = storage.getUser()?.name else {
|
|
handleFailure(with: Error.noUsername, using: transaction)
|
|
return promise
|
|
}
|
|
|
|
if let profileKey = storage.getUser()?.profileEncryptionKey?.keyData, let profilePictureURL = storage.getUser()?.profilePictureURL {
|
|
message.profile = VisibleMessage.Profile(displayName: name, profileKey: profileKey, profilePictureURL: profilePictureURL)
|
|
}
|
|
else {
|
|
message.profile = VisibleMessage.Profile(displayName: name)
|
|
}
|
|
}
|
|
|
|
// Convert it to protobuf
|
|
guard let proto = message.toProto(using: transaction) else {
|
|
handleFailure(with: Error.protoConversionFailed, using: transaction)
|
|
return promise
|
|
}
|
|
|
|
// Serialize the protobuf
|
|
let plaintext: Data
|
|
|
|
do {
|
|
plaintext = (try proto.serializedData() as NSData).paddedMessageBody()
|
|
}
|
|
catch {
|
|
SNLog("Couldn't serialize proto due to error: \(error).")
|
|
handleFailure(with: error, using: transaction)
|
|
return promise
|
|
}
|
|
|
|
// Encrypt the serialized protobuf
|
|
let ciphertext: Data
|
|
|
|
do {
|
|
ciphertext = try encryptWithSessionBlindingProtocol(plaintext, for: recipientBlindedPublicKey, openGroupPublicKey: openGroupPublicKey, using: dependencies)
|
|
}
|
|
catch {
|
|
SNLog("Couldn't encrypt message for destination: \(destination) due to error: \(error).")
|
|
handleFailure(with: error, using: transaction)
|
|
return promise
|
|
}
|
|
|
|
// Send the result
|
|
|
|
OpenGroupAPI
|
|
.send(
|
|
ciphertext,
|
|
toInboxFor: recipientBlindedPublicKey,
|
|
on: server,
|
|
using: dependencies
|
|
)
|
|
.done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in
|
|
dependencies.storage.write { transaction in
|
|
MessageSender.handleSuccessfulMessageSend(message, to: destination, using: transaction)
|
|
seal.fulfill(())
|
|
}
|
|
}
|
|
.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
|
dependencies.storage.write { transaction in
|
|
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
|
|
}
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
// MARK: Success & Failure Handling
|
|
public static func handleSuccessfulMessageSend(_ message: Message, to destination: Message.Destination, serverTimestamp: UInt64? = nil, isSyncMessage: Bool = false, using transaction: Any) {
|
|
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
|
// Get the visible message if possible
|
|
if let tsMessage = TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) {
|
|
// When the sync message is successfully sent, the hash value of this TSOutgoingMessage
|
|
// will be replaced by the hash value of the sync message. Since the hash value of the
|
|
// real message has no use when we delete a message. It is OK to let it be.
|
|
tsMessage.serverHash = message.serverHash
|
|
// Track the open group server message ID and update server timestamp
|
|
if let openGroupServerMessageID = message.openGroupServerMessageID, let timestamp = serverTimestamp {
|
|
// Use server timestamp for open group messages
|
|
// Otherwise the quote messages may not be able
|
|
// to be found by the timestamp on other devices
|
|
tsMessage.updateOpenGroupServerID(openGroupServerMessageID, serverTimeStamp: timestamp)
|
|
}
|
|
// Mark the message as sent
|
|
var recipients = [ message.recipient! ]
|
|
if case .closedGroup(_) = destination, let threadID = message.threadID, // threadID should always be set at this point
|
|
let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction), thread.isClosedGroup {
|
|
recipients = thread.groupModel.groupMemberIds
|
|
}
|
|
recipients.forEach { recipient in
|
|
tsMessage.update(withSentRecipient: recipient, wasSentByUD: true, transaction: transaction)
|
|
}
|
|
tsMessage.save(with: transaction)
|
|
NotificationCenter.default.post(name: .messageSentStatusDidChange, object: nil, userInfo: nil)
|
|
// Start the disappearing messages timer if needed
|
|
OWSDisappearingMessagesJob.shared().startAnyExpiration(for: tsMessage, expirationStartedAt: NSDate.millisecondTimestamp(), transaction: transaction)
|
|
}
|
|
// Prevent the same ExpirationTimerUpdate to be handled twice
|
|
if let message = message as? ExpirationTimerUpdate {
|
|
Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp!, using: transaction)
|
|
}
|
|
// Sync the message if:
|
|
// • it's a visible message or an expiration timer update
|
|
// • the destination was a contact
|
|
// • we didn't sync it already
|
|
let userPublicKey = getUserHexEncodedPublicKey()
|
|
if case .contact(let publicKey) = destination, !isSyncMessage {
|
|
if let message = message as? VisibleMessage { message.syncTarget = publicKey }
|
|
if let message = message as? ExpirationTimerUpdate { message.syncTarget = publicKey }
|
|
// FIXME: Make this a job
|
|
sendToSnodeDestination(.contact(publicKey: userPublicKey), message: message, using: transaction, isSyncMessage: true).retainUntilComplete()
|
|
}
|
|
}
|
|
|
|
public static func handleFailedMessageSend(_ message: Message, with error: Swift.Error, using transaction: Any) {
|
|
guard let tsMessage = TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) else { return }
|
|
// Remove the message timestamps if it fails
|
|
Storage.shared.removeReceivedMessageTimestamps([message.sentTimestamp!], using: transaction)
|
|
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
|
tsMessage.update(sendingError: error, transaction: transaction)
|
|
MessageInvalidator.invalidate(tsMessage, with: transaction)
|
|
}
|
|
}
|