// // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import LibSignalClient public class OutgoingStorySentMessageTranscript: OWSOutgoingSyncMessage { // Exposed to ObjC and made optional for MTLModel serialization @objc private var storyEncodedRecipientStates: Data? @objc private var storyMessageUniqueId: String? @objc private var isRecipientUpdate: NSNumber! public init(localThread: TSThread, timestamp: UInt64, recipientStates: [ServiceId: StoryRecipientState], transaction: SDSAnyReadTransaction) { // We need to store the encoded data rather than just the uniqueId // of the story message as the story message will have been deleted // by the time we're sending this transcript. self.storyEncodedRecipientStates = Self.encodeRecipientStates(recipientStates) self.isRecipientUpdate = NSNumber(value: true) super.init(timestamp: timestamp, thread: localThread, transaction: transaction) } public init(localThread: TSThread, storyMessage: StoryMessage, transaction: SDSAnyReadTransaction) { self.storyMessageUniqueId = storyMessage.uniqueId self.isRecipientUpdate = NSNumber(value: false) super.init(timestamp: storyMessage.timestamp, thread: localThread, transaction: transaction) } private static func encodeRecipientStates(_ recipientStates: [ServiceId: StoryRecipientState]) -> Data? { return try? JSONEncoder().encode(recipientStates.mapKeys(injectiveTransform: { $0.codableUppercaseString })) } private static func decodeRecipientStates(_ encodedRecipientStates: Data?) -> [ServiceId: StoryRecipientState]? { guard let encodedRecipientStates else { return nil } return (try? JSONDecoder().decode( [ServiceIdUppercaseString: StoryRecipientState].self, from: encodedRecipientStates ))?.mapKeys(injectiveTransform: { $0.wrappedValue }) } public override var isUrgent: Bool { false } private func storyMessage(transaction: SDSAnyReadTransaction) -> StoryMessage? { guard let storyMessageUniqueId = storyMessageUniqueId else { return nil } return StoryMessage.anyFetch(uniqueId: storyMessageUniqueId, transaction: transaction) } public override func syncMessageBuilder(transaction: SDSAnyReadTransaction) -> SSKProtoSyncMessageBuilder? { let sentBuilder = SSKProtoSyncMessageSent.builder() sentBuilder.setTimestamp(timestamp) sentBuilder.setIsRecipientUpdate(isRecipientUpdate.boolValue) if let storyMessage = storyMessage(transaction: transaction) { if !isRecipientUpdate.boolValue { guard let storyMessageProto = storyMessageProto(for: storyMessage, transaction: transaction) else { owsFailDebug("Failed to build sync proto for story message with timestamp \(storyMessage.timestamp)") return nil } sentBuilder.setStoryMessage(storyMessageProto) } guard case .outgoing(let recipientStates) = storyMessage.manifest else { owsFailDebug("Unexpected type for story message sync with timestamp \(storyMessage.timestamp)") return nil } applyRecipientStates(recipientStates, sentBuilder: sentBuilder) } else if let recipientStates = Self.decodeRecipientStates(storyEncodedRecipientStates) { applyRecipientStates(recipientStates, sentBuilder: sentBuilder) } else { owsFailDebug("Missing recipient states") return nil } do { let sentProto = try sentBuilder.build() let builder = SSKProtoSyncMessage.builder() builder.setSent(sentProto) return builder } catch { owsFailDebug("failed to build proto \(error)") return nil } } private func applyRecipientStates(_ recipientStates: [ServiceId: StoryRecipientState], sentBuilder: SSKProtoSyncMessageSentBuilder) { for (serviceId, state) in recipientStates { let builder = SSKProtoSyncMessageSentStoryMessageRecipient.builder() builder.setDestinationServiceID(serviceId.serviceIdString) builder.setDistributionListIds(state.contexts.map { $0.uuidString }) builder.setIsAllowedToReply(state.allowsReplies) sentBuilder.addStoryMessageRecipients(builder.buildInfallibly()) } } private func storyMessageProto(for storyMessage: StoryMessage, transaction: SDSAnyReadTransaction) -> SSKProtoStoryMessage? { let builder = SSKProtoStoryMessage.builder() switch storyMessage.attachment { case .file, .foreignReferenceAttachment: guard let attachmentReference = DependenciesBridge.shared.tsResourceStore.mediaAttachment( for: storyMessage, tx: transaction.asV2Read ), let attachment = attachmentReference.fetch(tx: transaction), let pointer = attachment.asTransitTierPointer() else { owsFailDebug("Missing attachment for outgoing story message") return nil } guard let attachmentProto = DependenciesBridge.shared.tsResourceManager.buildProtoForSending( from: attachmentReference, pointer: pointer ) else { owsFailDebug("Missing attachment for outgoing story message") return nil } builder.setFileAttachment(attachmentProto) if let storyMediaCaption = attachmentReference.storyMediaCaption { builder.setBodyRanges(storyMediaCaption.toProtoBodyRanges()) } case .text(let attachment): guard let attachmentProto = try? attachment.buildProto( parentStoryMessage: storyMessage, bodyRangeHandler: builder.setBodyRanges(_:), transaction: transaction ) else { owsFailDebug("Missing attachment for outgoing story message") return nil } builder.setTextAttachment(attachmentProto) } builder.setAllowsReplies(true) do { if let groupId = storyMessage.groupId, let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: transaction), let groupModel = groupThread.groupModel as? TSGroupModelV2 { builder.setGroup(try groupsV2.buildGroupContextV2Proto(groupModel: groupModel, changeActionsProtoData: nil)) } return try builder.build() } catch { owsFailDebug("failed to build protobuf: \(error)") return nil } } // MARK: - MTLModel public required init?(coder: NSCoder) { super.init(coder: coder) } public required init(dictionary: [String: Any]) throws { try super.init(dictionary: dictionary) } }