Signal-iOS/SignalServiceKit/Messages/DeviceSyncing/DeleteForMe/DeleteForMeOutgoingSyncMessage.swift

186 lines
7.0 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
/// Represents a sync message, being sent from this device, related to "delete
/// for me" actions.
///
/// - SeeAlso ``DeleteForMeOutgoingSyncMessageManager``
@objc(DeleteForMeOutgoingSyncMessage)
class DeleteForMeOutgoingSyncMessage: OWSOutgoingSyncMessage {
typealias Outgoing = DeleteForMeSyncMessage.Outgoing
struct Contents: Codable {
let messageDeletes: [Outgoing.MessageDeletes]
let conversationDeletes: [Outgoing.ConversationDelete]
let localOnlyConversationDelete: [Outgoing.LocalOnlyConversationDelete]
fileprivate var asProto: SSKProtoSyncMessageDeleteForMe {
let protoBuilder = SSKProtoSyncMessageDeleteForMe.builder()
protoBuilder.setMessageDeletes(messageDeletes.map { $0.asProto })
protoBuilder.setConversationDeletes(conversationDeletes.map { $0.asProto })
protoBuilder.setLocalOnlyConversationDeletes(localOnlyConversationDelete.map { $0.asProto })
return protoBuilder.buildInfallibly()
}
}
/// A JSON-serialized ``Contents`` struct.
///
/// - Important: The ObjC name must not change, for Mantle compatibility.
/// - Note
/// Nullability is intentional, since Mantle will set this property via its
/// reflection-based `init(coder:)` when we call `super.init(coder:)`.
@objc(contents)
private(set) var contentsData: Data!
init?(
contents: Contents,
thread: TSThread,
tx: SDSAnyReadTransaction
) {
do {
self.contentsData = try JSONEncoder().encode(contents)
} catch {
owsFailDebug("Failed to encode sync message contents!")
return nil
}
super.init(thread: thread, transaction: tx)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
required init(dictionary dictionaryValue: [String: Any]!) throws {
try super.init(dictionary: dictionaryValue)
}
override public var isUrgent: Bool { false }
override public func syncMessageBuilder(transaction: SDSAnyReadTransaction) -> SSKProtoSyncMessageBuilder? {
let contents: Contents
do {
contents = try JSONDecoder().decode(Contents.self, from: contentsData)
} catch let error {
owsFailDebug("Failed to decode serialized sync message contents! \(error)")
return nil
}
let syncMessageBuilder = SSKProtoSyncMessage.builder()
syncMessageBuilder.setDeleteForMe(contents.asProto)
return syncMessageBuilder
}
}
// MARK: -
extension DeleteForMeSyncMessage.Outgoing {
enum ConversationIdentifier: Codable, Equatable {
case threadServiceId(serviceId: String)
case threadE164(e164: String)
case threadGroupId(groupId: Data)
fileprivate var asProto: SSKProtoSyncMessageDeleteForMeConversationIdentifier {
let protoBuilder = SSKProtoSyncMessageDeleteForMeConversationIdentifier.builder()
switch self {
case .threadServiceId(let serviceId): protoBuilder.setThreadServiceID(serviceId)
case .threadE164(let e164): protoBuilder.setThreadE164(e164)
case .threadGroupId(let groupId): protoBuilder.setThreadGroupID(groupId)
}
return protoBuilder.buildInfallibly()
}
}
struct AddressableMessage: Codable, Equatable {
enum Author: Codable, Equatable {
/// The author's ACI. Note that the author of a message must be an
/// ACI, never a PNI.
case aci(aci: String)
/// The author's E164, if their ACI is absent. This should only be
/// relevant for old (pre-ACI) messages.
case e164(e164: String)
}
let author: Author
let sentTimestamp: UInt64
private init(author: Author, sentTimestamp: UInt64) {
self.author = author
self.sentTimestamp = sentTimestamp
}
#if TESTABLE_BUILD
static func forTests(author: Author, sentTimestamp: UInt64) -> AddressableMessage {
return AddressableMessage(author: author, sentTimestamp: sentTimestamp)
}
#endif
init?(incomingMessage: TSIncomingMessage) {
if let authorAci = incomingMessage.authorAddress.aci {
author = .aci(aci: authorAci.serviceIdUppercaseString)
} else if let authorE164 = incomingMessage.authorAddress.e164 {
author = .e164(e164: authorE164.stringValue)
} else {
return nil
}
sentTimestamp = incomingMessage.timestamp
}
init(outgoingMessage: TSOutgoingMessage, localIdentifiers: LocalIdentifiers) {
author = .aci(aci: localIdentifiers.aci.serviceIdUppercaseString)
sentTimestamp = outgoingMessage.timestamp
}
fileprivate var asProto: SSKProtoSyncMessageDeleteForMeAddressableMessage {
let protoBuilder = SSKProtoSyncMessageDeleteForMeAddressableMessage.builder()
protoBuilder.setSentTimestamp(sentTimestamp)
switch author {
case .aci(let aci): protoBuilder.setAuthorServiceID(aci)
case .e164(let e164): protoBuilder.setAuthorE164(e164)
}
return protoBuilder.buildInfallibly()
}
}
struct MessageDeletes: Codable, Equatable {
let conversationIdentifier: ConversationIdentifier
let addressableMessages: [AddressableMessage]
fileprivate var asProto: SSKProtoSyncMessageDeleteForMeMessageDeletes {
let protoBuilder = SSKProtoSyncMessageDeleteForMeMessageDeletes.builder()
protoBuilder.setConversation(conversationIdentifier.asProto)
protoBuilder.setMessages(addressableMessages.map { $0.asProto })
return protoBuilder.buildInfallibly()
}
}
struct ConversationDelete: Codable, Equatable {
let conversationIdentifier: ConversationIdentifier
let mostRecentAddressableMessages: [AddressableMessage]
let isFullDelete: Bool
fileprivate var asProto: SSKProtoSyncMessageDeleteForMeConversationDelete {
let protoBuilder = SSKProtoSyncMessageDeleteForMeConversationDelete.builder()
protoBuilder.setConversation(conversationIdentifier.asProto)
protoBuilder.setMostRecentMessages(mostRecentAddressableMessages.map { $0.asProto })
protoBuilder.setIsFullDelete(isFullDelete)
return protoBuilder.buildInfallibly()
}
}
struct LocalOnlyConversationDelete: Codable, Equatable {
let conversationIdentifier: ConversationIdentifier
fileprivate var asProto: SSKProtoSyncMessageDeleteForMeLocalOnlyConversationDelete {
let protoBuilder = SSKProtoSyncMessageDeleteForMeLocalOnlyConversationDelete.builder()
protoBuilder.setConversation(conversationIdentifier.asProto)
return protoBuilder.buildInfallibly()
}
}
}