// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import LibSignalClient public class OWSIncomingSentMessageTranscript: Dependencies, SentMessageTranscript { public let type: SentMessageTranscriptType public var requiredProtocolVersion: UInt32? public let timestamp: UInt64 public let recipientStates: [SignalServiceAddress: TSOutgoingMessageRecipientState] public static func from( sentProto: SSKProtoSyncMessageSent, serverTimestamp: UInt64, tx: DBWriteTransaction ) -> OWSIncomingSentMessageTranscript? { let isEdit = sentProto.editMessage?.dataMessage != nil guard let dataMessage = sentProto.message ?? sentProto.editMessage?.dataMessage else { owsFailDebug("Missing message.") return nil } guard sentProto.timestamp != 0 else { owsFailDebug("Sent missing timestamp.") return nil } let recipientAddress: SignalServiceAddress? let groupId: Data? if let groupContextV2 = dataMessage.groupV2 { guard let masterKey = groupContextV2.masterKey, masterKey.count >= 1 else { owsFailDebug("Missing masterKey.") return nil } guard let contextInfo = try? Self.groupsV2.groupV2ContextInfo(forMasterKeyData: masterKey) else { owsFailDebug("Couldn't parse contextInfo.") return nil } groupId = contextInfo.groupId recipientAddress = nil guard let groupId, groupId.count >= 1, GroupManager.isValidGroupId(groupId, groupsVersion: .V2) else { owsFailDebug("Invalid groupId.") return nil } } else if sentProto.destinationServiceID != nil || sentProto.destinationE164 != nil { let destinationAddress = SignalServiceAddress.legacyAddress( serviceIdString: sentProto.destinationServiceID, phoneNumber: sentProto.destinationE164 ) guard destinationAddress.isValid else { owsFailDebug("Invalid destinationAddress.") return nil } groupId = nil recipientAddress = destinationAddress } else { owsFailDebug("Neither a group ID nor recipient address found!") return nil } if let groupId { TSGroupThread.ensureGroupIdMapping(forGroupId: groupId, transaction: SDSDB.shimOnlyBridge(tx)) } var isExpirationTimerUpdate = false var isEndSessionMessage = false if dataMessage.hasFlags { let flags = Int32(dataMessage.flags) isExpirationTimerUpdate = (flags & SSKProtoDataMessageFlags.expirationTimerUpdate.rawValue) != 0 isEndSessionMessage = (flags & SSKProtoDataMessageFlags.endSession.rawValue) != 0 } let type: SentMessageTranscriptType if sentProto.isRecipientUpdate && !isEdit { guard let groupId, let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: SDSDB.shimOnlyBridge(tx)) else { owsFailDebug("We should never receive a 'recipient update' for messages in contact threads.") return nil } type = .recipientUpdate(groupThread) } else if isExpirationTimerUpdate { guard let target = getTarget( recipientAddress: recipientAddress, groupId: groupId, dataMessage: dataMessage, tx: tx ) else { return nil } type = .expirationTimerUpdate(target) } else if isEndSessionMessage { guard let recipientAddress else { owsFailDebug("We should never receive a 'end session' for messages in group threads.") return nil } type = .endSessionUpdate(TSContactThread.getOrCreateThread(contactAddress: recipientAddress)) } else if dataMessage.payment != nil { guard let target = getTarget( recipientAddress: recipientAddress, groupId: groupId, dataMessage: dataMessage, tx: tx ) else { return nil } guard let paymentModels = TSPaymentModels.parsePaymentProtos(dataMessage: dataMessage, thread: target.thread) else { return nil } let paymentServerTimestamp: UInt64 if serverTimestamp > 0 { paymentServerTimestamp = serverTimestamp } else { // We fall back to the sent timestamp, even though this is called a server timestamp. // This was done historically and behavior is maintained. paymentServerTimestamp = sentProto.timestamp } owsAssertDebug(paymentServerTimestamp > 0) let paymentNotification = SentMessageTranscriptType.PaymentNotification( target: target, serverTimestamp: paymentServerTimestamp, notification: paymentModels.notification ) type = .paymentNotification(paymentNotification) } else { guard let target = getTarget( recipientAddress: recipientAddress, groupId: groupId, dataMessage: dataMessage, tx: tx ) else { return nil } guard let messageParams = self.parseMessageParams( sentProto: sentProto, serverTimestamp: serverTimestamp, dataMessage: dataMessage, target: target, tx: tx ) else { return nil } type = .message(messageParams) } var recipientStates = [SignalServiceAddress: TSOutgoingMessageRecipientState]() for statusProto in sentProto.unidentifiedStatus { guard let serviceIdString = statusProto.destinationServiceID, let serviceId = try? ServiceId.parseFrom(serviceIdString: serviceIdString), statusProto.hasUnidentified else { owsFailDebug("Delivery status proto is missing value.") continue } let recipientState: TSOutgoingMessageRecipientState = TSOutgoingMessageRecipientState() recipientState.state = .sent recipientState.wasSentByUD = statusProto.unidentified recipientStates[.init(serviceId: serviceId, e164: nil)] = recipientState } guard validateTimestampsMatch(type: type, sentProto: sentProto, dataMessage: dataMessage) else { return nil } return .init( type: type, timestamp: sentProto.timestamp, recipientStates: recipientStates ) } private static func validateTimestampsMatch( type: SentMessageTranscriptType, sentProto: SSKProtoSyncMessageSent, dataMessage: SSKProtoDataMessage ) -> Bool { switch type { case .message, .expirationTimerUpdate, .paymentNotification: // We only validate these types break case .recipientUpdate, .endSessionUpdate: // Don't validate these types, as was done historically. return true } guard sentProto.timestamp == dataMessage.timestamp else { owsFailDebug("Transcript timestamps do not match, discarding message.") // This transcript is invalid, discard it. return false } return true } private static func parseMessageParams( sentProto: SSKProtoSyncMessageSent, serverTimestamp: UInt64, dataMessage: SSKProtoDataMessage, target: SentMessageTranscriptTarget, tx: DBWriteTransaction ) -> SentMessageTranscriptType.Message? { let isViewOnceMessage = dataMessage.hasIsViewOnce && dataMessage.isViewOnce let requiredProtocolVersion = dataMessage.requiredProtocolVersion == 0 ? nil : dataMessage.requiredProtocolVersion let body = dataMessage.body let bodyRanges: MessageBodyRanges? if dataMessage.bodyRanges.isEmpty.negated { bodyRanges = MessageBodyRanges(protos: dataMessage.bodyRanges) } else { bodyRanges = nil } let contact = OWSContact.contact(for: dataMessage, transaction: SDSDB.shimOnlyBridge(tx)) let linkPreviewBuilder: OwnedAttachmentBuilder? do { if let linkPreview = dataMessage.preview.first { linkPreviewBuilder = try DependenciesBridge.shared.linkPreviewManager.validateAndBuildLinkPreview( from: linkPreview, dataMessage: dataMessage, tx: tx ) } else { linkPreviewBuilder = nil } } catch { Logger.error("linkPreviewError: \(error)") linkPreviewBuilder = nil } let giftBadge = OWSGiftBadge.maybeBuild(from: dataMessage) if giftBadge != nil, target.thread.isGroupThread { owsFailDebug("Ignoring gift sent to group") return nil } let messageStickerBuilder: OwnedAttachmentBuilder? do { messageStickerBuilder = try MessageSticker.buildValidatedMessageSticker( dataMessage: dataMessage, transaction: SDSDB.shimOnlyBridge(tx) ) } catch { if !MessageSticker.isNoStickerError(error) { owsFailDebug("stickerError: \(error)") } messageStickerBuilder = nil } let quotedMessageBuilder = DependenciesBridge.shared.quotedReplyManager.quotedMessage( for: dataMessage, thread: target.thread, tx: tx ) let storyTimestamp: UInt64? let storyAuthorAci: Aci? if let storyContext = dataMessage.storyContext, storyContext.hasSentTimestamp, let authorAci = storyContext.authorAci { storyTimestamp = storyContext.sentTimestamp storyAuthorAci = try? Aci.parseFrom(serviceIdString: authorAci) if storyAuthorAci == nil { owsFailDebug("Discarding story reply transcript with invalid aci") return nil } } else { storyTimestamp = nil storyAuthorAci = nil } return .init( target: target, body: body, bodyRanges: bodyRanges, attachmentPointerProtos: dataMessage.attachments, quotedMessageBuilder: quotedMessageBuilder, contact: contact, linkPreviewBuilder: linkPreviewBuilder, giftBadge: giftBadge, messageStickerBuilder: messageStickerBuilder, isViewOnceMessage: isViewOnceMessage, expirationStartedAt: sentProto.expirationStartTimestamp, expirationDuration: dataMessage.expireTimer, storyTimestamp: storyTimestamp, storyAuthorAci: storyAuthorAci ) } private static func getTarget( recipientAddress: SignalServiceAddress?, groupId: Data?, dataMessage: SSKProtoDataMessage, tx: DBWriteTransaction ) -> SentMessageTranscriptTarget? { if let groupId { guard let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: SDSDB.shimOnlyBridge(tx)) else { owsFailDebug("Missing thread for group.") return nil } if let groupContextV2 = dataMessage.groupV2 { guard groupThread.isGroupV2Thread else { owsFailDebug("Invalid thread for v2 group.") return nil } guard groupContextV2.hasRevision else { owsFailDebug("Missing revision.") return nil } let revision = groupContextV2.revision guard let groupModel = groupThread.groupModel as? TSGroupModelV2, revision <= groupModel.revision else { owsFailDebug("Unexpected revision.") return nil } } else { owsFailDebug("Missing group context.") return nil } return .group(groupThread) } else if let recipientAddress { let thread = TSContactThread.getOrCreateThread( withContactAddress: recipientAddress, transaction: SDSDB.shimOnlyBridge(tx) ) return .contact(thread, DisappearingMessageToken.token(forProtoExpireTimer: dataMessage.expireTimer)) } else { return nil } } private init( type: SentMessageTranscriptType, requiredProtocolVersion: UInt32? = nil, timestamp: UInt64, recipientStates: [SignalServiceAddress: TSOutgoingMessageRecipientState] ) { self.type = type self.requiredProtocolVersion = requiredProtocolVersion self.timestamp = timestamp self.recipientStates = recipientStates } }