import CocoaLumberjackSwift
import Foundation
import PromiseKit
import ThreemaEssentials
import ThreemaProtocols
final class MessageStore: MessageStoreProtocol {
private let frameworkInjector: FrameworkInjectorProtocol
private let messageProcessorDelegate: MessageProcessorDelegate
init(
frameworkInjector: FrameworkInjectorProtocol,
messageProcessorDelegate: MessageProcessorDelegate
) {
self.frameworkInjector = frameworkInjector
self.messageProcessorDelegate = messageProcessorDelegate
}
@available(*, deprecated, message: "Just for incoming (deprecated) audio message, blob origin is always public!")
func save(
audioMessage: BoxAudioMessage,
conversationIdentity: String,
createdAt: Date,
reflectedAt: Date
) throws {
messageProcessorDelegate.incomingMessageStarted(audioMessage)
let (conversation, _) = try conversationSender(forMessage: audioMessage, isOutgoing: false)
guard let msg = try frameworkInjector.entityManager.getOrCreateMessage(
for: audioMessage,
sender: nil,
conversation: conversation,
thumbnail: nil, myIdentity: frameworkInjector.myIdentityStore.identity
) as? AudioMessageEntity else {
throw MediatorReflectedProcessorError
.messageNotProcessed(message: "Could not find/create audio message in DB")
}
frameworkInjector.entityManager.performAndWaitSave {
msg.date = createdAt
msg.remoteSentDate = createdAt
}
assert(audioMessage.fromIdentity == conversationIdentity)
messageProcessorDelegate.incomingMessageChanged(audioMessage, baseMessageEntity: msg)
messageProcessorDelegate.incomingMessageFinished(audioMessage)
}
func save(
fileMessage: BoxFileMessage,
conversationIdentity: String,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool,
timeoutDownloadThumbnail: Int
) -> Promise<Void> {
Promise { seal in
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(fileMessage)
}
let (conversation, _) = try conversationSender(forMessage: fileMessage, isOutgoing: isOutgoing)
FileMessageDecoder.decodeMessage(
fromBox: fileMessage,
sender: nil,
conversation: conversation,
isReflectedMessage: true,
timeoutDownloadThumbnail: Int32(timeoutDownloadThumbnail),
entityManager: self.frameworkInjector.entityManager,
onCompletion: { baseMessageEntityObject in
self.frameworkInjector.entityManager.performAndWaitSave {
guard let baseMessageEntity = baseMessageEntityObject as? BaseMessageEntity else {
fatalError("Parameter baseMessageEntityObject must be type of `BaseMessageEntity`")
}
baseMessageEntity.id = fileMessage.messageID
baseMessageEntity.date = createdAt
baseMessageEntity.remoteSentDate = isOutgoing ? reflectedAt : createdAt
}
if !isOutgoing {
if let baseMessageEntity = baseMessageEntityObject as? BaseMessageEntity {
assert(fileMessage.fromIdentity == conversationIdentity)
self.messageProcessorDelegate.incomingMessageChanged(
fileMessage,
baseMessageEntity: baseMessageEntity
)
}
self.messageProcessorDelegate.incomingMessageFinished(fileMessage)
}
else {
self.messageProcessorDelegate.changedManagedObjectID(conversation.objectID)
if let baseMessageEntity = baseMessageEntityObject as? BaseMessageEntity {
self.messageProcessorDelegate.changedManagedObjectID(baseMessageEntity.objectID)
}
}
seal.fulfill_()
}
) { error in
seal.reject(
MediatorReflectedProcessorError.messageNotProcessed(
message: "Could not process file message: \(error?.localizedDescription ?? "")"
)
)
}
}
}
func save(
textMessage: BoxTextMessage,
conversationIdentity: String,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool
) throws {
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(textMessage)
}
let (conversation, _) = try conversationSender(forMessage: textMessage, isOutgoing: isOutgoing)
guard let msg = try frameworkInjector.entityManager.getOrCreateMessage(
for: textMessage,
sender: nil,
conversation: conversation,
thumbnail: nil, myIdentity: frameworkInjector.myIdentityStore.identity
) as? TextMessageEntity else {
throw MediatorReflectedProcessorError.messageNotProcessed(message: "Could not find/create text message")
}
frameworkInjector.entityManager.performAndWaitSave {
msg.date = createdAt
msg.remoteSentDate = isOutgoing ? reflectedAt : createdAt
}
if !isOutgoing {
assert(conversationIdentity == textMessage.fromIdentity)
messageProcessorDelegate.incomingMessageChanged(textMessage, baseMessageEntity: msg)
messageProcessorDelegate.incomingMessageFinished(textMessage)
}
else {
messageProcessorDelegate.changedManagedObjectID(conversation.objectID)
messageProcessorDelegate.changedManagedObjectID(msg.objectID)
}
}
func save(contactDeletePhotoMessage amsg: ContactDeletePhotoMessage) {
frameworkInjector.contactStore.deleteProfilePicture(amsg.fromIdentity, shouldReflect: false)
frameworkInjector.contactStore.removeProfilePictureRequest(amsg.fromIdentity)
changedContact(with: amsg.fromIdentity)
}
func save(contactSetPhotoMessage: ContactSetPhotoMessage) -> Promise<Void> {
syncLoadBlob(
blobID: contactSetPhotoMessage.blobID,
encryptionKey: contactSetPhotoMessage.encryptionKey,
origin: .local
)
.then { (data: Data?) -> Promise<Void> in
guard let data else {
return Promise()
}
self.frameworkInjector.entityManager.performAndWait {
let contact = self.frameworkInjector.entityManager.entityFetcher
.contactEntity(for: contactSetPhotoMessage.fromIdentity)
self.frameworkInjector.contactStore.updateProfilePicture(
contact?.identity,
imageData: data,
shouldReflect: false,
blobID: nil,
encryptionKey: nil,
didFailWithError: nil
)
}
self.changedContact(with: contactSetPhotoMessage.fromIdentity)
return Promise()
}
}
func save(
deliveryReceiptMessage: DeliveryReceiptMessage,
createdAt: Date,
isOutgoing: Bool
) throws {
var messageReadConversations = Set<ConversationEntity>()
for id in deliveryReceiptMessage.receiptMessageIDs {
if let messageID = id as? Data {
try frameworkInjector.entityManager.performAndWait {
if let conversation = self.frameworkInjector.entityManager.conversation(
forMessage: deliveryReceiptMessage, myIdentity: self.frameworkInjector.myIdentityStore.identity
),
let msg = self.frameworkInjector.entityManager.entityFetcher.message(
with: messageID,
in: conversation
) {
try self.frameworkInjector.entityManager.performAndWaitSave {
if deliveryReceiptMessage.receiptType == .received {
DDLogNotice("Message ID \(msg.id.hexString) has been received by recipient")
msg.delivered = true
msg.deliveryDate = createdAt
}
else if deliveryReceiptMessage.receiptType == .read {
DDLogNotice("Message ID \(msg.id.hexString) has been read by recipient")
msg.read = true
msg.readDate = createdAt
DDLogVerbose("Message marked as read: \(msg.id.hexString)")
messageReadConversations.insert(msg.conversation)
// If it is a read receipt of a reflected incoming message, then remove all
// notifications of
// this message
if isOutgoing {
if let contentKey = PendingUserNotificationKey.key(
identity: deliveryReceiptMessage.toIdentity,
messageID: messageID
) {
self.frameworkInjector.userNotificationCenterManager.remove(
contentKey: contentKey,
exceptStage: nil,
justPending: false
)
}
}
}
else if deliveryReceiptMessage.receiptType == .ack || deliveryReceiptMessage
.receiptType == .decline {
let reactionsProcessor = ReactionsMessageProcessor(
entityManager: self.frameworkInjector.entityManager,
messageProcessorDelegate: self.messageProcessorDelegate
)
try reactionsProcessor.handleLegacyReaction(
ack: deliveryReceiptMessage.receiptType == .ack,
date: createdAt,
messageID: msg.objectID,
sender: deliveryReceiptMessage.fromIdentity
)
}
else {
DDLogWarn(
"Unknown delivery receipt type \(deliveryReceiptMessage.receiptType) with message ID \(messageID.hexString)"
)
}
}
self.messageProcessorDelegate.changedManagedObjectID(msg.objectID)
if !isOutgoing {
// Clean up user notifications, just in case message delivery receipt was pushed
self.messageProcessorDelegate.incomingMessageFinished(deliveryReceiptMessage)
}
}
else {
throw MediatorReflectedProcessorError.messageNotProcessed(
message: "Unable to store delivery receipt (\(deliveryReceiptMessage.receiptType)) for message \(messageID.hexString) because conversation or message was not found"
)
}
}
}
}
if !messageReadConversations.isEmpty {
messageProcessorDelegate.readMessage(inConversations: messageReadConversations)
}
}
func save(deleteMessage: DeleteMessage, createdAt: Date, isOutgoing: Bool) throws {
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(deleteMessage)
}
let (conversation, _) = try conversationSender(forMessage: deleteMessage, isOutgoing: isOutgoing)
let relatedObjectIDs = frameworkInjector.entityManager.relatedObjectIDsForAbstractDeleteMessage(
deleteMessage,
in: conversation
)
let msg = try frameworkInjector.entityManager.deleteMessage(
for: deleteMessage,
conversation: conversation,
myIdentity: frameworkInjector.myIdentityStore.identity
)
if !isOutgoing {
if let relatedObjectIDs {
for relatedObjectID in relatedObjectIDs {
messageProcessorDelegate.changedManagedObjectID(relatedObjectID)
}
}
messageProcessorDelegate.incomingMessageChanged(deleteMessage, baseMessageEntity: msg)
messageProcessorDelegate.incomingMessageFinished(deleteMessage)
}
conversation.updateLastDisplayMessage(with: frameworkInjector.entityManager)
}
func save(deleteGroupMessage: DeleteGroupMessage, createdAt: Date, isOutgoing: Bool) throws {
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(deleteGroupMessage)
}
let (conversation, _) = try conversationSender(forMessage: deleteGroupMessage, isOutgoing: isOutgoing)
let relatedObjectIDs = frameworkInjector.entityManager.relatedObjectIDsForAbstractDeleteMessage(
deleteGroupMessage,
in: conversation
)
let msg = try frameworkInjector.entityManager.deleteMessage(
for: deleteGroupMessage,
conversation: conversation,
myIdentity: frameworkInjector.myIdentityStore.identity
)
if !isOutgoing {
if let relatedObjectIDs {
for relatedObjectID in relatedObjectIDs {
messageProcessorDelegate.changedManagedObjectID(relatedObjectID)
}
}
messageProcessorDelegate.incomingMessageChanged(deleteGroupMessage, baseMessageEntity: msg)
messageProcessorDelegate.incomingMessageFinished(deleteGroupMessage)
}
conversation.updateLastDisplayMessage(with: frameworkInjector.entityManager)
}
func save(editMessage: EditMessage, createdAt: Date, isOutgoing: Bool) throws {
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(editMessage)
}
let (conversation, _) = try conversationSender(forMessage: editMessage, isOutgoing: isOutgoing)
let msg = try frameworkInjector.entityManager.editMessage(
for: editMessage,
conversation: conversation,
myIdentity: frameworkInjector.myIdentityStore.identity
)
if !isOutgoing {
messageProcessorDelegate.incomingMessageChanged(editMessage, baseMessageEntity: msg)
messageProcessorDelegate.incomingMessageFinished(editMessage)
}
else {
if let entries = msg.historyEntries {
for entry in entries {
messageProcessorDelegate.changedManagedObjectID(entry.objectID)
}
}
}
}
func save(editGroupMessage: EditGroupMessage, createdAt: Date, isOutgoing: Bool) throws {
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(editGroupMessage)
}
let (conversation, _) = try conversationSender(forMessage: editGroupMessage, isOutgoing: isOutgoing)
let msg = try frameworkInjector.entityManager.editMessage(
for: editGroupMessage,
conversation: conversation,
myIdentity: frameworkInjector.myIdentityStore.identity
)
if !isOutgoing {
messageProcessorDelegate.incomingMessageChanged(editGroupMessage, baseMessageEntity: msg)
messageProcessorDelegate.incomingMessageFinished(editGroupMessage)
}
else {
if let entries = msg.historyEntries {
for entry in entries {
messageProcessorDelegate.changedManagedObjectID(entry.objectID)
}
}
}
}
@available(
*,
deprecated,
message: "Just for incoming (deprecated) audio group message, blob origin is always public!"
)
func save(
groupAudioMessage: GroupAudioMessage,
senderIdentity: String,
createdAt: Date,
reflectedAt: Date
) throws {
messageProcessorDelegate.incomingMessageStarted(groupAudioMessage)
let (conversation, sender) = try conversationSender(forMessage: groupAudioMessage, isOutgoing: false)
guard let sender else {
throw MediatorReflectedProcessorError.senderNotFound(identity: senderIdentity)
}
guard let msg = try frameworkInjector.entityManager.getOrCreateMessage(
for: groupAudioMessage,
sender: sender,
conversation: conversation,
thumbnail: nil, myIdentity: frameworkInjector.myIdentityStore.identity
) as? AudioMessageEntity else {
throw MediatorReflectedProcessorError
.messageNotProcessed(message: "Could not find/create group audio message in DB")
}
frameworkInjector.entityManager.performAndWaitSave {
msg.date = createdAt
msg.remoteSentDate = createdAt
}
assert(groupAudioMessage.fromIdentity == senderIdentity)
messageProcessorDelegate.incomingMessageChanged(groupAudioMessage, baseMessageEntity: msg)
messageProcessorDelegate.incomingMessageFinished(groupAudioMessage)
}
func save(
groupCreateMessage amsg: GroupCreateMessage,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool
) throws -> Promise<Void> {
// Validate members, all members must be known contacts, otherwise device is not in sync
try frameworkInjector.entityManager.performAndWait {
for identity in (amsg.groupMembers as! [String])
.filter({ $0 != self.frameworkInjector.myIdentityStore.identity }) {
guard self.frameworkInjector.entityManager.entityFetcher.contactEntity(for: identity) != nil else {
DDLogWarn("Unknown group member \(identity), device not in sync anymore")
throw MediatorReflectedProcessorError.contactNotFound(identity: identity)
}
}
}
let groupIdentity = GroupIdentity(id: amsg.groupID, creator: ThreemaIdentity(amsg.groupCreator))
DDLogNotice("Reflected group-setup \(groupIdentity) with \(amsg.groupMembers?.count ?? 0) members")
return Promise { seal in
Task {
do {
let systemMessageDate: Date =
if !isOutgoing {
createdAt
}
else {
reflectedAt
}
guard try await frameworkInjector.groupManager.createOrUpdateDB(
for: groupIdentity,
members: Set<String>(amsg.groupMembers.map { $0 as! String }),
systemMessageDate: systemMessageDate,
sourceCaller: .sync
) != nil else {
seal.reject(MediatorReflectedProcessorError.groupCreateFailed(groupIdentity: groupIdentity))
return
}
self.changedConversationAndGroupEntity(
groupID: amsg.groupID,
groupCreatorIdentity: amsg.groupCreator
)
seal.fulfill_()
}
catch {
seal.reject(error)
}
}
}
}
func save(
groupDeletePhotoMessage amsg: GroupDeletePhotoMessage,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool
) -> Promise<Void> {
Promise { seal in
Task {
let sentDate: Date =
if !isOutgoing {
createdAt
}
else {
reflectedAt
}
do {
try await frameworkInjector.groupManager.deletePhoto(
groupID: amsg.groupID,
creator: amsg.groupCreator,
sentDate: sentDate,
send: false
)
self.changedConversationAndGroupEntity(
groupID: amsg.groupID,
groupCreatorIdentity: amsg.groupCreator
)
seal.fulfill_()
}
catch {
seal.reject(error)
}
}
}
}
func save(
groupLeaveMessage amsg: GroupLeaveMessage,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool
) {
let systemMessageDate: Date =
if !isOutgoing {
createdAt
}
else {
reflectedAt
}
frameworkInjector.groupManager.leaveDB(
groupID: amsg.groupID,
creator: amsg.groupCreator,
member: amsg.fromIdentity,
systemMessageDate: systemMessageDate
)
changedConversationAndGroupEntity(groupID: amsg.groupID, groupCreatorIdentity: amsg.groupCreator)
}
func save(
groupRenameMessage amsg: GroupRenameMessage,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool
) -> Promise<Void> {
Promise { seal in
Task {
do {
let systemMessageDate: Date =
if !isOutgoing {
createdAt
}
else {
reflectedAt
}
try await frameworkInjector.groupManager.setName(
groupID: amsg.groupID,
creator: amsg.groupCreator,
name: amsg.name,
systemMessageDate: systemMessageDate,
send: false
)
self.changedConversationAndGroupEntity(
groupID: amsg.groupID,
groupCreatorIdentity: amsg.groupCreator
)
seal.fulfill_()
}
catch {
seal.reject(error)
}
}
}
}
func save(
groupFileMessage: GroupFileMessage,
senderIdentity: String,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool,
timeoutDownloadThumbnail: Int
) -> Promise<Void> {
Promise { seal in
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(groupFileMessage)
}
let (conversation, sender) = try conversationSender(forMessage: groupFileMessage, isOutgoing: isOutgoing)
guard isOutgoing || (!isOutgoing && sender != nil) else {
throw MediatorReflectedProcessorError.senderNotFound(identity: senderIdentity)
}
FileMessageDecoder.decodeGroupMessage(
fromBox: groupFileMessage,
sender: sender,
conversation: conversation,
isReflectedMessage: true,
timeoutDownloadThumbnail: Int32(timeoutDownloadThumbnail),
entityManager: self.frameworkInjector.entityManager,
onCompletion: { baseMessageEntityObject in
self.frameworkInjector.entityManager.performAndWaitSave {
guard let baseMessageEntity = baseMessageEntityObject as? BaseMessageEntity else {
fatalError("Parameter baseMessageEntityObject must be type of `BaseMessageEntity`")
}
baseMessageEntity.id = groupFileMessage.messageID
baseMessageEntity.date = createdAt
if !isOutgoing {
baseMessageEntity.deliveryDate = reflectedAt
baseMessageEntity.remoteSentDate = createdAt
}
else {
baseMessageEntity.remoteSentDate = reflectedAt
}
}
if !isOutgoing {
if let baseMessageEntity = baseMessageEntityObject as? BaseMessageEntity {
assert(groupFileMessage.fromIdentity == senderIdentity)
self.messageProcessorDelegate.incomingMessageChanged(
groupFileMessage,
baseMessageEntity: baseMessageEntity
)
}
self.messageProcessorDelegate.incomingMessageFinished(groupFileMessage)
}
else {
self.messageProcessorDelegate.changedManagedObjectID(conversation.objectID)
if let baseMessageEntity = baseMessageEntityObject as? BaseMessageEntity {
self.messageProcessorDelegate.changedManagedObjectID(baseMessageEntity.objectID)
}
}
seal.fulfill_()
}
) { error in
seal.reject(
MediatorReflectedProcessorError.messageNotProcessed(
message: "Could not process group file message: \(error?.localizedDescription ?? "")"
)
)
}
}
}
@available(
*,
deprecated,
message: "Just for incoming (deprecated) image (group) message, blob origin is always public!"
)
func save(
imageMessage: AbstractMessage,
senderIdentity: String,
createdAt: Date,
reflectedAt: Date,
maxBytesToDecrypt: Int
) throws -> Promise<Void> {
assert(imageMessage is BoxImageMessage || imageMessage is GroupImageMessage)
assert(senderIdentity == imageMessage.fromIdentity)
guard !(imageMessage is BoxImageMessage && imageMessage is GroupImageMessage) else {
throw MediatorReflectedProcessorError
.messageNotProcessed(message: "Wrong message type, must be BoxImageMessage or GroupImageMessage")
}
let (conversation, sender) = try conversationSender(forMessage: imageMessage, isOutgoing: false)
guard let sender else {
throw MediatorReflectedProcessorError.contactNotFound(identity: senderIdentity)
}
return Promise { seal in
messageProcessorDelegate.incomingMessageStarted(imageMessage)
guard let msg = try frameworkInjector.entityManager.getOrCreateMessage(
for: imageMessage,
sender: nil,
conversation: conversation,
thumbnail: nil, myIdentity: frameworkInjector.myIdentityStore.identity
) as? ImageMessageEntity else {
seal
.reject(
MediatorReflectedProcessorError
.messageNotProcessed(message: "Could not find/create (group) image message in DB")
)
return
}
let (senderPublicKey, messageID, conversationObjectID, blobID, blobOrigin, encryptionKey, nonce): (
Data,
Data,
NSManagedObjectID,
Data?,
BlobOrigin,
Data?,
Data?
) = frameworkInjector.entityManager.performAndWaitSave {
msg.date = createdAt
msg.remoteSentDate = createdAt
return (
sender.publicKey,
msg.id,
conversation.objectID,
msg.blobIdentifier,
msg.blobOrigin,
msg.encryptionKey,
msg.imageNonce
)
}
guard let blobID else {
seal.reject(MediatorReflectedProcessorError.messageNotProcessed(message: "BlobID of image is nil."))
return
}
// Create image message in DB and download and decrypt blob
assert(imageMessage.fromIdentity == senderIdentity)
self.messageProcessorDelegate.incomingMessageChanged(imageMessage, baseMessageEntity: msg)
let downloadQueue = DispatchQueue.global(qos: .default)
let processor = ImageMessageProcessor(
blobDownloader: BlobDownloader(blobURL: BlobURL(
serverConnector: self.frameworkInjector.serverConnector,
userSettings: self.frameworkInjector.userSettings,
queue: DispatchQueue.global(qos: .userInitiated)
), queue: downloadQueue),
myIdentityStore: self.frameworkInjector.myIdentityStore,
userSettings: self.frameworkInjector.userSettings,
entityManager: self.frameworkInjector.entityManager
)
processor.downloadImage(
imageMessageID: messageID,
in: conversationObjectID,
imageBlobID: blobID,
origin: blobOrigin,
imageBlobEncryptionKey: encryptionKey,
imageBlobNonce: nonce,
senderPublicKey: senderPublicKey,
maxBytesToDecrypt: maxBytesToDecrypt
)
.done {
assert(imageMessage.fromIdentity == senderIdentity)
self.messageProcessorDelegate.incomingMessageChanged(imageMessage, baseMessageEntity: msg)
seal.fulfill_()
}
.ensure {
self.messageProcessorDelegate.incomingMessageFinished(imageMessage)
}
.catch { error in
seal.reject(error)
}
}
}
func save(
groupLocationMessage: GroupLocationMessage,
senderIdentity: String,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool
) throws {
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(groupLocationMessage)
}
let (conversation, sender) = try conversationSender(forMessage: groupLocationMessage, isOutgoing: isOutgoing)
guard isOutgoing || (!isOutgoing && sender != nil) else {
throw MediatorReflectedProcessorError.contactNotFound(identity: senderIdentity)
}
guard let msg = try frameworkInjector.entityManager.getOrCreateMessage(
for: groupLocationMessage,
sender: sender,
conversation: conversation,
thumbnail: nil, myIdentity: frameworkInjector.myIdentityStore.identity
) as? LocationMessageEntity else {
throw MediatorReflectedProcessorError
.messageNotProcessed(message: "Could not find/create group location message in DB")
}
frameworkInjector.entityManager.performAndWaitSave {
msg.date = createdAt
if !isOutgoing {
msg.deliveryDate = reflectedAt
msg.remoteSentDate = createdAt
}
else {
msg.remoteSentDate = reflectedAt
}
}
// Caution this is async
setPoiAddress(message: msg)
.done {
if !isOutgoing {
assert(groupLocationMessage.fromIdentity == senderIdentity)
self.messageProcessorDelegate.incomingMessageChanged(groupLocationMessage, baseMessageEntity: msg)
self.messageProcessorDelegate.incomingMessageFinished(groupLocationMessage)
}
else {
self.messageProcessorDelegate.changedManagedObjectID(conversation.objectID)
self.messageProcessorDelegate.changedManagedObjectID(msg.objectID)
}
}
.catch { error in
DDLogError("Set POI address failed \(error.localizedDescription)")
}
}
func save(
groupBallotCreateMessage: GroupBallotCreateMessage,
senderIdentity: String,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool
) throws -> Promise<Void> {
Promise { seal in
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(groupBallotCreateMessage)
}
let (conversation, sender) = try conversationSender(
forMessage: groupBallotCreateMessage,
isOutgoing: isOutgoing
)
guard isOutgoing || (!isOutgoing && sender != nil) else {
throw MediatorReflectedProcessorError.senderNotFound(identity: groupBallotCreateMessage.fromIdentity)
}
let decoder = BallotMessageDecoder(self.frameworkInjector.entityManager)
decoder?.decodeCreateBallot(
fromGroupBox: groupBallotCreateMessage,
sender: sender,
conversation: conversation, onCompletion: { ballotMessageEntityObject in
Task {
guard let ballotMessageEntity = ballotMessageEntityObject as? BallotMessageEntity else {
fatalError("ballotMessageEntityObject must be type of `BallotMessageEntity`")
}
await self.frameworkInjector.entityManager.performSave {
ballotMessageEntity.isOwn = NSNumber(booleanLiteral: isOutgoing)
ballotMessageEntity.date = createdAt
if !isOutgoing {
ballotMessageEntity.deliveryDate = reflectedAt
ballotMessageEntity.remoteSentDate = createdAt
}
else {
ballotMessageEntity.remoteSentDate = reflectedAt
}
}
if !isOutgoing {
assert(groupBallotCreateMessage.fromIdentity == senderIdentity)
self.messageProcessorDelegate.incomingMessageChanged(
groupBallotCreateMessage,
baseMessageEntity: ballotMessageEntity
)
self.messageProcessorDelegate.incomingMessageFinished(groupBallotCreateMessage)
}
else {
self.messageProcessorDelegate.changedManagedObjectID(conversation.objectID)
self.messageProcessorDelegate.changedManagedObjectID(ballotMessageEntity.objectID)
if let ballot = ballotMessageEntity.ballot {
self.messageProcessorDelegate.changedManagedObjectID(ballot.objectID)
}
}
seal.fulfill_()
}
},
onError: { error in
seal.reject(error)
}
)
}
}
func save(groupBallotVoteMessage: GroupBallotVoteMessage) throws {
try frameworkInjector.entityManager.performAndWaitSave {
if let decoder = BallotMessageDecoder(self.frameworkInjector.entityManager),
!decoder.decodeVote(fromGroupBox: groupBallotVoteMessage) {
throw MediatorReflectedProcessorError
.messageDecodeFailed(message: groupBallotVoteMessage.loggingDescription)
}
}
changedBallot(with: groupBallotVoteMessage.ballotID)
}
func save(
groupSetPhotoMessage amsg: GroupSetPhotoMessage,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool
) -> Promise<Void> {
syncLoadBlob(blobID: amsg.blobID, encryptionKey: amsg.encryptionKey, origin: .public)
.then { (data: Data?) -> Promise<Void> in
guard let data else {
return Promise()
}
return Promise { seal in
Task {
do {
try await self.frameworkInjector.groupManager.setPhoto(
groupID: amsg.groupID,
creator: amsg.groupCreator,
imageData: data,
sentDate: amsg.date,
send: false
)
seal.fulfill_()
}
catch {
seal.reject(error)
}
}
}
}
}
func save(
groupDeliveryReceiptMessage: GroupDeliveryReceiptMessage,
createdAt: Date,
isOutgoing: Bool
) throws {
guard groupDeliveryReceiptMessage.receiptType == ReceiptType.ack.rawValue ||
groupDeliveryReceiptMessage.receiptType == ReceiptType.decline.rawValue else {
DDLogWarn("Unknown group delivery receipt type \(groupDeliveryReceiptMessage.receiptType)")
return
}
let receiptType: GroupDeliveryReceipt.DeliveryReceiptType =
groupDeliveryReceiptMessage.receiptType == ReceiptType.ack.rawValue ? .acknowledged : .declined
for id in groupDeliveryReceiptMessage.receiptMessageIDs {
if let messageID = id as? Data {
try frameworkInjector.entityManager.performAndWait {
guard let conversation = self.frameworkInjector.entityManager.conversation(
forMessage: groupDeliveryReceiptMessage,
myIdentity: self.frameworkInjector.myIdentityStore.identity
) else {
throw MediatorReflectedProcessorError
.conversationNotFound(message: groupDeliveryReceiptMessage.loggingDescription)
}
if let msg = self.frameworkInjector.entityManager.entityFetcher.message(
with: messageID,
in: conversation
),
msg.conversation.groupID == groupDeliveryReceiptMessage.groupID {
try self.frameworkInjector.entityManager.performAndWaitSave {
if receiptType == .acknowledged || receiptType == .declined {
let reactionsProcessor = ReactionsMessageProcessor(
entityManager: self.frameworkInjector.entityManager,
messageProcessorDelegate: self.messageProcessorDelegate
)
try reactionsProcessor.handleLegacyReaction(
ack: receiptType == .acknowledged,
date: createdAt,
messageID: msg.objectID,
sender: groupDeliveryReceiptMessage.fromIdentity
)
}
else {
let receipt = GroupDeliveryReceipt(
identity: groupDeliveryReceiptMessage.fromIdentity,
deliveryReceiptType: receiptType,
date: createdAt
)
msg.add(groupDeliveryReceipt: receipt)
}
}
self.messageProcessorDelegate.changedManagedObjectID(msg.objectID)
if !isOutgoing {
// Clean up user notifications, just in case group message delivery receipt was pushed
self.messageProcessorDelegate.incomingMessageFinished(groupDeliveryReceiptMessage)
}
}
else {
throw MediatorReflectedProcessorError
.messageNotProcessed(message: groupDeliveryReceiptMessage.loggingDescription)
}
}
}
}
}
func save(
groupTextMessage: GroupTextMessage,
senderIdentity: String,
messageID: Data,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool
) throws {
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(groupTextMessage)
}
let (conversation, sender) = try conversationSender(forMessage: groupTextMessage, isOutgoing: isOutgoing)
guard isOutgoing || (!isOutgoing && sender != nil) else {
throw MediatorReflectedProcessorError.senderNotFound(identity: groupTextMessage.fromIdentity)
}
guard let msg = try frameworkInjector.entityManager.getOrCreateMessage(
for: groupTextMessage,
sender: sender,
conversation: conversation,
thumbnail: nil, myIdentity: frameworkInjector.myIdentityStore.identity
) as? TextMessageEntity else {
throw MediatorReflectedProcessorError.messageNotProcessed(message: "Could not find/create text message")
}
frameworkInjector.entityManager.performAndWaitSave {
msg.date = createdAt
if !isOutgoing {
msg.delivered = true
msg.deliveryDate = reflectedAt
msg.remoteSentDate = createdAt
}
else {
msg.remoteSentDate = reflectedAt
}
}
if !isOutgoing {
assert(groupTextMessage.fromIdentity == senderIdentity)
messageProcessorDelegate.incomingMessageChanged(groupTextMessage, baseMessageEntity: msg)
messageProcessorDelegate.incomingMessageFinished(groupTextMessage)
}
else {
messageProcessorDelegate.changedManagedObjectID(conversation.objectID)
messageProcessorDelegate.changedManagedObjectID(msg.objectID)
}
}
@available(
*,
deprecated,
message: "Just for incoming (deprecated) video (group) message, blob origin is always public!"
)
func save(
videoMessage: AbstractMessage,
senderIdentity: String,
createdAt: Date,
reflectedAt: Date,
maxBytesToDecrypt: Int
) throws -> Promise<Void> {
assert(videoMessage is BoxVideoMessage || videoMessage is GroupVideoMessage)
guard !(videoMessage is BoxVideoMessage && videoMessage is GroupVideoMessage) else {
throw MediatorReflectedProcessorError
.messageNotProcessed(message: "Wrong message type, must be BoxVideoMessage or GroupVideoMessage")
}
let (conversation, sender) = try conversationSender(forMessage: videoMessage, isOutgoing: false)
guard let sender else {
throw MediatorReflectedProcessorError.contactNotFound(identity: senderIdentity)
}
return Promise { seal in
messageProcessorDelegate.incomingMessageStarted(videoMessage)
// Save message first and after try download thumbnail
guard let msg = try frameworkInjector.entityManager.getOrCreateMessage(
for: videoMessage,
sender: sender,
conversation: conversation,
thumbnail: UIImage(named: "threema.video.fill"), myIdentity: frameworkInjector.myIdentityStore.identity
) as? VideoMessageEntity else {
throw MediatorReflectedProcessorError
.messageNotProcessed(message: "Could not find/create (group) video message")
}
var thumbnailBlobID: Data!
if let videoMessage = videoMessage as? BoxVideoMessage {
thumbnailBlobID = videoMessage.thumbnailBlobID
}
else if let videoMessage = videoMessage as? GroupVideoMessage {
thumbnailBlobID = videoMessage.thumbnailBlobID
}
let (conversationObjectID, messageID, blobOrigin) = frameworkInjector.entityManager.performAndWaitSave {
msg.date = createdAt
msg.remoteSentDate = createdAt
return (conversation.objectID, msg.id, msg.blobOrigin)
}
assert(videoMessage.fromIdentity == senderIdentity)
self.messageProcessorDelegate.incomingMessageChanged(videoMessage, baseMessageEntity: msg)
// A VideoMessage never has a local blob because all note group capable devices send everything as
// FileMessage (-> localOrigin: false)
// Download, decrypt and save blob of thumbnail
let downloadQueue = DispatchQueue.global(qos: .default)
let videoProcessor = VideoMessageProcessor(
blobDownloader: BlobDownloader(blobURL: BlobURL(
serverConnector: self.frameworkInjector.serverConnector,
userSettings: self.frameworkInjector.userSettings,
queue: DispatchQueue.global(qos: .userInitiated)
), queue: downloadQueue),
userSettings: self.frameworkInjector.userSettings,
entityManager: self.frameworkInjector.entityManager
)
videoProcessor.downloadVideoThumbnail(
videoMessageID: messageID,
in: conversationObjectID,
origin: blobOrigin,
thumbnailBlobID: thumbnailBlobID,
maxBytesToDecrypt: maxBytesToDecrypt
)
.done {
assert(videoMessage.fromIdentity == senderIdentity)
self.messageProcessorDelegate.incomingMessageChanged(videoMessage, baseMessageEntity: msg)
seal.fulfill_()
}
.ensure {
self.messageProcessorDelegate.incomingMessageFinished(videoMessage)
}
.catch { error in
seal.reject(error)
}
}
}
func save(
locationMessage: BoxLocationMessage,
conversationIdentity: String,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool
) throws {
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(locationMessage)
}
let (conversation, _) = try conversationSender(forMessage: locationMessage, isOutgoing: isOutgoing)
guard let msg = try frameworkInjector.entityManager.getOrCreateMessage(
for: locationMessage,
sender: nil,
conversation: conversation,
thumbnail: nil, myIdentity: frameworkInjector.myIdentityStore.identity
) as? LocationMessageEntity else {
throw MediatorReflectedProcessorError.messageNotProcessed(message: "Could not find/create location message")
}
frameworkInjector.entityManager.performAndWaitSave {
msg.date = createdAt
msg.remoteSentDate = isOutgoing ? reflectedAt : createdAt
msg.delivered = true
}
// Caution this is async
setPoiAddress(message: msg)
.done {
if !isOutgoing {
assert(locationMessage.fromIdentity == conversationIdentity)
self.messageProcessorDelegate.incomingMessageChanged(locationMessage, baseMessageEntity: msg)
self.messageProcessorDelegate.incomingMessageFinished(locationMessage)
}
else {
self.messageProcessorDelegate.changedManagedObjectID(conversation.objectID)
self.messageProcessorDelegate.changedManagedObjectID(msg.objectID)
}
}
.catch { error in
DDLogError("Set POI address failed \(error)")
}
}
func save(
ballotCreateMessage: BoxBallotCreateMessage,
conversationIdentity: String,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool
) throws -> Promise<Void> {
Promise { seal in
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(ballotCreateMessage)
}
let (conversation, sender) = try conversationSender(forMessage: ballotCreateMessage, isOutgoing: isOutgoing)
guard isOutgoing || (!isOutgoing && sender != nil) else {
throw MediatorReflectedProcessorError.senderNotFound(identity: ballotCreateMessage.fromIdentity)
}
let decoder = BallotMessageDecoder(self.frameworkInjector.entityManager)
decoder?.decodeCreateBallot(
fromBox: ballotCreateMessage,
sender: sender,
conversation: conversation,
onCompletion: { ballotMessageEntityObject in
Task {
guard let ballotMessageEntity = ballotMessageEntityObject as? BallotMessageEntity else {
fatalError("ballotMessageEntityObject must be type of `BallotMessageEntity`")
}
await self.frameworkInjector.entityManager.performSave {
ballotMessageEntity.isOwn = NSNumber(booleanLiteral: isOutgoing)
ballotMessageEntity.date = createdAt
ballotMessageEntity.remoteSentDate = isOutgoing ? reflectedAt : createdAt
if !isOutgoing {
assert(ballotCreateMessage.fromIdentity == conversationIdentity)
self.messageProcessorDelegate.incomingMessageChanged(
ballotCreateMessage,
baseMessageEntity: ballotMessageEntity
)
self.messageProcessorDelegate.incomingMessageFinished(ballotCreateMessage)
}
else {
self.messageProcessorDelegate.changedManagedObjectID(conversation.objectID)
self.messageProcessorDelegate.changedManagedObjectID(ballotMessageEntity.objectID)
if let ballot = ballotMessageEntity.ballot {
self.messageProcessorDelegate.changedManagedObjectID(ballot.objectID)
}
}
}
seal.fulfill_()
}
},
onError: { error in
seal.reject(error)
}
)
}
}
func save(ballotVoteMessage: BoxBallotVoteMessage) throws {
try frameworkInjector.entityManager.performAndWaitSave {
if let decoder = BallotMessageDecoder(self.frameworkInjector.entityManager),
!decoder.decodeVote(fromBox: ballotVoteMessage) {
throw MediatorReflectedProcessorError
.messageDecodeFailed(message: ballotVoteMessage.loggingDescription)
}
}
changedBallot(with: ballotVoteMessage.ballotID)
}
func save(
groupCallStartMessage: GroupCallStartMessage,
senderIdentity: String,
createdAt: Date,
reflectedAt: Date,
isOutgoing: Bool
) throws -> Promise<Void> {
guard let decodedCallStartMessage = groupCallStartMessage.decoded
else {
throw MediatorReflectedProcessorError.messageNotProcessed(message: groupCallStartMessage.loggingDescription)
}
return Promise { seal in
Task {
let groupIdentity = GroupIdentity(
id: groupCallStartMessage.groupID,
creator: ThreemaIdentity(groupCallStartMessage.groupCreator)
)
let conversationObjectID = await self.frameworkInjector.entityManager.perform {
self.frameworkInjector.entityManager.entityFetcher.conversationEntity(
for: groupIdentity, myIdentity: self.frameworkInjector.myIdentityStore.identity
)?.objectID
}
guard let conversationObjectID else {
seal.reject(
MediatorReflectedProcessorError.messageNotProcessed(
message: groupCallStartMessage.loggingDescription
)
)
return
}
guard let fromIdentity = groupCallStartMessage.fromIdentity else {
seal.reject(
MediatorReflectedProcessorError.messageNotProcessed(
message: groupCallStartMessage.loggingDescription
)
)
return
}
let receiveDate: Date =
if !isOutgoing {
createdAt
}
else {
reflectedAt
}
// We also need to set the date on the abstract message, otherwise the notification content is not
// correct here. This is because the content is derived from an abstract message, not a base message.
groupCallStartMessage.date = receiveDate
let callMightBeRunning = await GlobalGroupCallManagerSingleton.shared.handleMessage(
rawMessage: decodedCallStartMessage,
from: fromIdentity,
in: conversationObjectID,
receiveDate: receiveDate
)
// Handling the message peeks the SFU. Remember the verdict, it is used to suppress the user
// notification for a call that already ended.
groupCallStartMessage.callEnded = !callMightBeRunning
if !isOutgoing {
self.messageProcessorDelegate.incomingMessageFinished(groupCallStartMessage)
}
self.messageProcessorDelegate.changedManagedObjectID(conversationObjectID)
seal.fulfill_()
}
}
}
func save(
reactionMessage: ReactionMessage,
conversationIdentity: String,
createdAt: Date,
isOutgoing: Bool
) throws {
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(reactionMessage)
}
let reactionManager = ReactionsMessageProcessor(
entityManager: frameworkInjector.entityManager,
messageProcessorDelegate: messageProcessorDelegate
)
let (conversation, _) = try conversationSender(forMessage: reactionMessage, isOutgoing: isOutgoing)
let msg = reactionManager.handleMessage(abstractMessage: reactionMessage, conversation: conversation)
if !isOutgoing {
assert(reactionMessage.fromIdentity == conversationIdentity)
if let msg {
messageProcessorDelegate.incomingMessageChanged(reactionMessage, baseMessageEntity: msg)
}
messageProcessorDelegate.incomingMessageFinished(reactionMessage)
}
else {
if let msg {
messageProcessorDelegate.changedManagedObjectID(msg.objectID)
}
}
}
func save(
groupReactionMessage: GroupReactionMessage,
senderIdentity: String,
createdAt: Date,
isOutgoing: Bool
) throws {
if !isOutgoing {
messageProcessorDelegate.incomingMessageStarted(groupReactionMessage)
}
let reactionManager = ReactionsMessageProcessor(
entityManager: frameworkInjector.entityManager,
messageProcessorDelegate: messageProcessorDelegate
)
let (conversation, _) = try conversationSender(forMessage: groupReactionMessage, isOutgoing: isOutgoing)
let msg = reactionManager.handleGroupMessage(abstractMessage: groupReactionMessage, conversation: conversation)
if !isOutgoing {
assert(groupReactionMessage.fromIdentity == senderIdentity)
if let msg {
messageProcessorDelegate.incomingMessageChanged(groupReactionMessage, baseMessageEntity: msg)
}
messageProcessorDelegate.incomingMessageFinished(groupReactionMessage)
}
else {
if let msg {
messageProcessorDelegate.changedManagedObjectID(msg.objectID)
}
}
}
// MARK: Misc
/// Get poi-address, if is necessary.
/// - Parameter message:: LocationMessageEntity to save address for
private func setPoiAddress(message: LocationMessageEntity?) -> Promise<Void> {
Promise { seal in
self.frameworkInjector.entityManager.performAndWait {
if let message,
let msg = self.frameworkInjector.entityManager.entityFetcher
.managedObject(with: message.objectID) as? LocationMessageEntity,
msg.poiAddress == nil,
let latitude = Double(exactly: msg.latitude),
let longitude = Double(exactly: msg.longitude) {
let accuracy = Double(exactly: msg.accuracy ?? 0.0) ?? 0.0
ThreemaUtilityObjC.reverseGeocodeNearLatitude(
latitude,
longitude: longitude,
accuracy: accuracy,
completion: { geoLabel in
self.frameworkInjector.entityManager.performAndWaitSave {
message.poiAddress = geoLabel
}
seal.fulfill_()
}
) { error in
if let error {
DDLogWarn("Reverse geocoding failed: \(error)")
}
self.frameworkInjector.entityManager.performAndWaitSave {
message.poiAddress = String(format: "%.5f°, %.5f°", latitude, longitude)
}
seal.fulfill_()
}
}
else {
seal.fulfill_()
}
}
}
}
private func syncLoadBlob(blobID: Data, encryptionKey: Data, origin: BlobOrigin) -> Promise<Data?> {
Promise { seal in
let downloadQueue = DispatchQueue.global(qos: .default)
let blobDownloader = BlobDownloader(
blobURL: BlobURL(
serverConnector: self.frameworkInjector.serverConnector,
userSettings: self.frameworkInjector.userSettings,
queue: downloadQueue
),
queue: downloadQueue
)
blobDownloader.download(blobID: blobID, origin: origin) { data, error in
if let error = error as? NSError {
if error.code == 404 {
seal.fulfill(nil)
}
else {
seal.reject(MediatorReflectedProcessorError.downloadFailed(message: error.localizedDescription))
}
return
}
// Decrypt blob data
guard let imageData = NaClCrypto.shared()
.symmetricDecryptData(data, withKey: encryptionKey, nonce: ThreemaProtocol.nonce01) else {
seal.reject(MediatorReflectedProcessorError.downloadFailed(message: "Decrypt blob data failed"))
return
}
blobDownloader.markDownloadDone(for: blobID, origin: .local)
seal.fulfill(imageData)
}
}
}
private func changedBallot(with ballotID: Data) {
frameworkInjector.entityManager.performAndWait {
if let ballot = self.frameworkInjector.entityManager.entityFetcher.ballotEntity(for: ballotID) {
self.messageProcessorDelegate.changedManagedObjectID(ballot.objectID)
}
}
}
private func changedContact(with identity: String) {
frameworkInjector.entityManager.performAndWait {
if let contact = self.frameworkInjector.entityManager.entityFetcher.contactEntity(for: identity) {
self.messageProcessorDelegate.changedManagedObjectID(contact.objectID)
}
}
}
private func changedConversationAndGroupEntity(groupID: Data, groupCreatorIdentity: String) {
frameworkInjector.entityManager.performAndWait {
let groupIdentity = GroupIdentity(id: groupID, creator: ThreemaIdentity(groupCreatorIdentity))
if let conversation = self.frameworkInjector.entityManager.entityFetcher
.conversationEntity(for: groupIdentity, myIdentity: self.frameworkInjector.myIdentityStore.identity) {
self.messageProcessorDelegate.changedManagedObjectID(conversation.objectID)
if let groupEntity = self.frameworkInjector.entityManager.entityFetcher.groupEntity(for: conversation) {
self.messageProcessorDelegate.changedManagedObjectID(groupEntity.objectID)
}
}
}
}
private func conversationSender(
forMessage message: AbstractMessage,
isOutgoing: Bool
) throws -> (conversation: ConversationEntity, sender: ContactEntity?) {
var conversationIdentity: String?
var result = frameworkInjector.entityManager.existingConversationSenderReceiver(
for: message,
myIdentity: frameworkInjector.profileStore.profile.myIdentity.rawValue
)
if !isOutgoing {
guard let sender = result.sender else {
throw MediatorReflectedProcessorError.senderNotFound(identity: message.fromIdentity)
}
frameworkInjector.entityManager.performAndWait {
conversationIdentity = sender.identity
}
}
else if !(message is AbstractGroupMessage) {
guard let receiver = result.receiver else {
throw MediatorReflectedProcessorError.receiverNotFound(identity: message.toIdentity)
}
frameworkInjector.entityManager.performAndWait {
conversationIdentity = receiver.identity
}
}
if let conversationIdentity, result.conversation == nil, !(message is AbstractGroupMessage) {
frameworkInjector.entityManager.performAndWaitSave {
result.conversation = self.frameworkInjector.entityManager.conversation(
for: conversationIdentity,
createIfNotExisting: true
)
}
}
guard let conversation = result.conversation else {
throw MediatorReflectedProcessorError.conversationNotFound(message: message.loggingDescription)
}
return (conversation, result.sender)
}
}
¤ Dauer der Verarbeitung: 0.26 Sekunden
¤
*© Formatika GbR, Deutschland
|
|