Signal-iOS/SignalServiceKit/Messages/Stories/OutgoingStorySentMessageTranscript.swift
2026-03-24 16:11:09 -05:00

195 lines
8.3 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import LibSignalClient
public class OutgoingStorySentMessageTranscript: OutgoingSyncMessage {
override public class var supportsSecureCoding: Bool { true }
public required init?(coder: NSCoder) {
guard let isRecipientUpdate = coder.decodeObject(of: NSNumber.self, forKey: "isRecipientUpdate") else {
return nil
}
self.isRecipientUpdate = isRecipientUpdate.boolValue
self.storyEncodedRecipientStates = coder.decodeObject(of: NSData.self, forKey: "storyEncodedRecipientStates") as Data?
self.storyMessageUniqueId = coder.decodeObject(of: NSString.self, forKey: "storyMessageUniqueId") as String?
super.init(coder: coder)
}
override public func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(NSNumber(value: isRecipientUpdate), forKey: "isRecipientUpdate")
if let storyEncodedRecipientStates {
coder.encode(storyEncodedRecipientStates, forKey: "storyEncodedRecipientStates")
}
if let storyMessageUniqueId {
coder.encode(storyMessageUniqueId, forKey: "storyMessageUniqueId")
}
}
override public var hash: Int {
var hasher = Hasher()
hasher.combine(super.hash)
hasher.combine(isRecipientUpdate)
hasher.combine(storyEncodedRecipientStates)
hasher.combine(storyMessageUniqueId)
return hasher.finalize()
}
override public func isEqual(_ object: Any?) -> Bool {
guard let object = object as? Self else { return false }
guard super.isEqual(object) else { return false }
guard self.isRecipientUpdate == object.isRecipientUpdate else { return false }
guard self.storyEncodedRecipientStates == object.storyEncodedRecipientStates else { return false }
guard self.storyMessageUniqueId == object.storyMessageUniqueId else { return false }
return true
}
private let storyEncodedRecipientStates: Data?
private let storyMessageUniqueId: String?
private let isRecipientUpdate: Bool
public init(localThread: TSContactThread, timestamp: UInt64, recipientStates: [ServiceId: StoryRecipientState], transaction: DBReadTransaction) {
// 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.storyMessageUniqueId = nil
self.isRecipientUpdate = true
super.init(timestamp: timestamp, localThread: localThread, tx: transaction)
}
public init(localThread: TSContactThread, storyMessage: StoryMessage, transaction: DBReadTransaction) {
self.storyEncodedRecipientStates = nil
self.storyMessageUniqueId = storyMessage.uniqueId
self.isRecipientUpdate = false
super.init(timestamp: storyMessage.timestamp, localThread: localThread, tx: 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<ServiceId>: StoryRecipientState].self,
from: encodedRecipientStates,
))?.mapKeys(injectiveTransform: { $0.wrappedValue })
}
override public var isUrgent: Bool { false }
private func storyMessage(transaction: DBReadTransaction) -> StoryMessage? {
guard let storyMessageUniqueId else { return nil }
return StoryMessage.anyFetch(uniqueId: storyMessageUniqueId, transaction: transaction)
}
override public func syncMessageBuilder(tx: DBReadTransaction) -> SSKProtoSyncMessageBuilder? {
let sentBuilder = SSKProtoSyncMessageSent.builder()
sentBuilder.setTimestamp(timestamp)
sentBuilder.setIsRecipientUpdate(isRecipientUpdate)
if let storyMessage = storyMessage(transaction: tx) {
if !isRecipientUpdate {
guard let storyMessageProto = storyMessageProto(for: storyMessage, transaction: tx) 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.setDestinationServiceIDBinary(serviceId.serviceIdBinary)
builder.setDistributionListIds(state.contexts.map { $0.uuidString })
builder.setIsAllowedToReply(state.allowsReplies)
sentBuilder.addStoryMessageRecipients(builder.buildInfallibly())
}
}
private func storyMessageProto(for storyMessage: StoryMessage, transaction: DBReadTransaction) -> SSKProtoStoryMessage? {
let attachmentStore = DependenciesBridge.shared.attachmentStore
let builder = SSKProtoStoryMessage.builder()
switch storyMessage.attachment {
case .media:
guard
let storyMessageRowId = storyMessage.id,
let referencedAttachment = attachmentStore.fetchAnyReferencedAttachment(
for: .storyMessageMedia(storyMessageRowId: storyMessageRowId),
tx: transaction,
),
let attachmentProto = referencedAttachment.asProtoForSending()
else {
owsFailDebug("Missing or failed to build proto for attachment for outgoing story message")
return nil
}
builder.setFileAttachment(attachmentProto)
if let storyMediaCaption = referencedAttachment.reference.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 GroupsV2Protos.buildGroupContextProto(groupModel: groupModel, groupChangeProtoData: nil))
}
return try builder.build()
} catch {
owsFailDebug("failed to build protobuf: \(error)")
return nil
}
}
}