admin delete receive & send
This commit is contained in:
parent
38bbe659fa
commit
4a0aec54b3
@ -67,6 +67,8 @@
|
||||
047A6DD02E00B5720048EDF4 /* BackupKeyReminderMegaphoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */; };
|
||||
0480F0002E57C51A006CBB29 /* BackupsEnabledNotificationMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0480EFFF2E57C513006CBB29 /* BackupsEnabledNotificationMegaphone.swift */; };
|
||||
0484CECE2F44B7BE009AB2CB /* AdminDeleteRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0484CECD2F44B7BB009AB2CB /* AdminDeleteRecord.swift */; };
|
||||
0484CED02F44BD00009AB2CB /* AdminDeleteManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0484CECF2F44BCFB009AB2CB /* AdminDeleteManager.swift */; };
|
||||
0484CED22F44DD01009AB2CB /* OutgoingAdminDeleteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0484CED12F44DCFF009AB2CB /* OutgoingAdminDeleteMessage.swift */; };
|
||||
048D58362ED4FABB00F26237 /* PinDisappearingMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048D58352ED4FA8500F26237 /* PinDisappearingMessageViewController.swift */; };
|
||||
048D58382EDDD57E00F26237 /* OutgoingPinMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048D58372EDDD56D00F26237 /* OutgoingPinMessage.swift */; };
|
||||
04A19B432EEC815200CCB105 /* PinnedMessageExpirationJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A19B422EEC814900CCB105 /* PinnedMessageExpirationJob.swift */; };
|
||||
@ -4204,6 +4206,8 @@
|
||||
047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupKeyReminderMegaphoneTests.swift; sourceTree = "<group>"; };
|
||||
0480EFFF2E57C513006CBB29 /* BackupsEnabledNotificationMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsEnabledNotificationMegaphone.swift; sourceTree = "<group>"; };
|
||||
0484CECD2F44B7BB009AB2CB /* AdminDeleteRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminDeleteRecord.swift; sourceTree = "<group>"; };
|
||||
0484CECF2F44BCFB009AB2CB /* AdminDeleteManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminDeleteManager.swift; sourceTree = "<group>"; };
|
||||
0484CED12F44DCFF009AB2CB /* OutgoingAdminDeleteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingAdminDeleteMessage.swift; sourceTree = "<group>"; };
|
||||
048D58352ED4FA8500F26237 /* PinDisappearingMessageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinDisappearingMessageViewController.swift; sourceTree = "<group>"; };
|
||||
048D58372EDDD56D00F26237 /* OutgoingPinMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingPinMessage.swift; sourceTree = "<group>"; };
|
||||
04A19B422EEC814900CCB105 /* PinnedMessageExpirationJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessageExpirationJob.swift; sourceTree = "<group>"; };
|
||||
@ -8468,7 +8472,9 @@
|
||||
0484CECC2F44B7B4009AB2CB /* AdminDelete */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0484CECF2F44BCFB009AB2CB /* AdminDeleteManager.swift */,
|
||||
0484CECD2F44B7BB009AB2CB /* AdminDeleteRecord.swift */,
|
||||
0484CED12F44DCFF009AB2CB /* OutgoingAdminDeleteMessage.swift */,
|
||||
);
|
||||
path = AdminDelete;
|
||||
sourceTree = "<group>";
|
||||
@ -18855,6 +18861,7 @@
|
||||
D9EDF2782E4D2A2F001D4BEC /* AccountEntropyPoolManager.swift in Sources */,
|
||||
C14D49CE2D667F830033BA69 /* AccountKeyStore.swift in Sources */,
|
||||
50BE67532CAAF7DF006D7BC7 /* AdHocCallRecordManager.swift in Sources */,
|
||||
0484CED02F44BD00009AB2CB /* AdminDeleteManager.swift in Sources */,
|
||||
0484CECE2F44B7BE009AB2CB /* AdminDeleteRecord.swift in Sources */,
|
||||
728BFE4C2C5C3427008F20F1 /* Aes256Key.swift in Sources */,
|
||||
667DEE5F2BC7175300EFF32D /* AllMediaCategory.swift in Sources */,
|
||||
@ -19460,6 +19467,7 @@
|
||||
663F81D92E78C2740033D2AE /* OrphanedBackupAttachmentScheduler.swift in Sources */,
|
||||
668478F92CAB687600430D68 /* OrphanedBackupAttachmentStore.swift in Sources */,
|
||||
F9C5CD9F289453B400548EEE /* OutageDetection.swift in Sources */,
|
||||
0484CED22F44DD01009AB2CB /* OutgoingAdminDeleteMessage.swift in Sources */,
|
||||
50C55EB12F1ACCD700BA8309 /* OutgoingBlockedSyncMessage.swift in Sources */,
|
||||
6640639E294D20A900997E0B /* OutgoingCallEventSyncMessage.swift in Sources */,
|
||||
D979CC292AD3933B006AAC49 /* OutgoingCallEventSyncMessageManager.swift in Sources */,
|
||||
|
||||
@ -476,11 +476,26 @@ public class CVComponentBodyText: CVComponentBase, CVComponent {
|
||||
}
|
||||
|
||||
private var labelConfigForRemotelyDeleted: CVLabelConfig {
|
||||
let text = (
|
||||
isIncoming
|
||||
? OWSLocalizedString("THIS_MESSAGE_WAS_DELETED", comment: "text indicating the message was remotely deleted")
|
||||
: OWSLocalizedString("YOU_DELETED_THIS_MESSAGE", comment: "text indicating the message was remotely deleted by you"),
|
||||
)
|
||||
var text: String
|
||||
switch bodyText {
|
||||
case .remotelyDeleted(let deleteAuthor):
|
||||
if let deleteAuthor {
|
||||
// TODO: make attributed string with icon and tappable display name.
|
||||
let format = OWSLocalizedString(
|
||||
"DELETED_BY_ADMIN",
|
||||
comment: "Text indicating the message was remotely deleted by an admin. Embeds {{admin display name}}",
|
||||
)
|
||||
text = String(format: format, deleteAuthor)
|
||||
} else {
|
||||
fallthrough
|
||||
}
|
||||
default:
|
||||
text = (
|
||||
isIncoming
|
||||
? OWSLocalizedString("THIS_MESSAGE_WAS_DELETED", comment: "text indicating the message was remotely deleted")
|
||||
: OWSLocalizedString("YOU_DELETED_THIS_MESSAGE", comment: "text indicating the message was remotely deleted by you"),
|
||||
)
|
||||
}
|
||||
return CVLabelConfig(
|
||||
text: .text(text),
|
||||
displayConfig: .forUnstyledText(font: textMessageFont.italic(), textColor: bodyTextColor),
|
||||
|
||||
@ -133,7 +133,7 @@ public struct CVComponentState: Equatable {
|
||||
|
||||
// We use the "body text" component to
|
||||
// render the "remotely deleted" indicator.
|
||||
case remotelyDeleted
|
||||
case remotelyDeleted(deleteAuthorName: String?)
|
||||
|
||||
var displayableText: DisplayableText? {
|
||||
switch self {
|
||||
@ -1187,6 +1187,34 @@ private extension CVComponentState.Builder {
|
||||
return FailedOrPendingDownloads(attachmentPointers: attachmentPointers)
|
||||
}
|
||||
|
||||
/// If the message was deleted remotely by an admin, display the admin's name.
|
||||
private func displayNameForDeleteMessage(message: TSMessage) -> String? {
|
||||
let adminDeleteManager = DependenciesBridge.shared.adminDeleteManager
|
||||
|
||||
guard
|
||||
let authorAci = adminDeleteManager.adminDeleteAuthor(
|
||||
interactionId: interaction.sqliteRowId!,
|
||||
tx: transaction,
|
||||
)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if authorAci == localAci, message.isOutgoing {
|
||||
// Display usual self delete message for outgoing self-deletion.
|
||||
return nil
|
||||
} else if let incomingMessage = message as? TSIncomingMessage, incomingMessage.authorAddress.aci == authorAci {
|
||||
// Display usual other user delete for incoming self-deletion.
|
||||
return nil
|
||||
} else {
|
||||
// Only display admin name if non self-delete.
|
||||
return SSKEnvironment.shared.contactManagerRef.displayName(
|
||||
for: SignalServiceAddress(authorAci),
|
||||
tx: transaction,
|
||||
).resolvedValue()
|
||||
}
|
||||
}
|
||||
|
||||
mutating func populateAndBuild(
|
||||
message: TSMessage,
|
||||
revealedSpoilerIdsSnapshot: Set<StyleIdType>,
|
||||
@ -1194,7 +1222,9 @@ private extension CVComponentState.Builder {
|
||||
|
||||
if message.wasRemotelyDeleted {
|
||||
// If the message has been remotely deleted, suppress everything else.
|
||||
self.bodyText = .remotelyDeleted
|
||||
|
||||
let deleteAuthor = displayNameForDeleteMessage(message: message)
|
||||
self.bodyText = .remotelyDeleted(deleteAuthorName: deleteAuthor)
|
||||
return build()
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
public import SignalServiceKit
|
||||
public import SignalUI
|
||||
import LibSignalClient
|
||||
|
||||
public struct CVSelectionType: OptionSet {
|
||||
public let rawValue: UInt
|
||||
@ -312,6 +313,7 @@ extension ConversationViewController {
|
||||
|
||||
func didTapDeleteSelectedItems() {
|
||||
let db = DependenciesBridge.shared.db
|
||||
let adminDeleteManager = DependenciesBridge.shared.adminDeleteManager
|
||||
|
||||
let selectionItems = self.selectionState.selectionItems
|
||||
guard !selectionItems.isEmpty else {
|
||||
@ -361,10 +363,16 @@ extension ConversationViewController {
|
||||
|
||||
let canDeleteForEveryone: Bool = db.read { tx in
|
||||
selectionItems.allSatisfy { selectionItem in
|
||||
TSOutgoingMessage.fetchOutgoingMessageViaCache(
|
||||
uniqueId: selectionItem.interactionId,
|
||||
transaction: tx,
|
||||
)?.canBeRemotelyDeleted ?? false
|
||||
guard
|
||||
let message = TSMessage.fetchMessageViaCache(
|
||||
uniqueId: selectionItem.interactionId,
|
||||
transaction: tx,
|
||||
)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
let canAdminDelete = adminDeleteManager.canAdminDeleteMessage(message: message, thread: thread, tx: tx)
|
||||
return message.canBeRemotelyDeletedByNonAdmin || canAdminDelete
|
||||
}
|
||||
}
|
||||
|
||||
@ -429,18 +437,20 @@ extension ConversationViewController {
|
||||
thread: TSThread,
|
||||
tx: DBWriteTransaction,
|
||||
) {
|
||||
let adminDeleteManager = DependenciesBridge.shared.adminDeleteManager
|
||||
|
||||
guard !selectionItems.isEmpty else { return }
|
||||
guard let latestThread = TSThread.fetchViaCache(uniqueId: thread.uniqueId, transaction: tx) else {
|
||||
return owsFailDebug("Trying to delete messages without a thread.")
|
||||
}
|
||||
|
||||
guard let aci = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx)?.aci else {
|
||||
return owsFailDebug("Local ACI missing during message deletion.")
|
||||
guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx) else {
|
||||
return owsFailDebug("LocalIdentifiers missing during message deletion.")
|
||||
}
|
||||
|
||||
selectionItems.forEach {
|
||||
guard
|
||||
let message = TSOutgoingMessage.fetchOutgoingMessageViaCache(
|
||||
let message = TSMessage.fetchMessageViaCache(
|
||||
uniqueId: $0.interactionId,
|
||||
transaction: tx,
|
||||
)
|
||||
@ -448,20 +458,66 @@ extension ConversationViewController {
|
||||
return
|
||||
}
|
||||
|
||||
let deleteMessage = OutgoingDeleteMessage(thread: latestThread, message: message, tx: tx)
|
||||
let canAdminDelete = adminDeleteManager.canAdminDeleteMessage(message: message, thread: thread, tx: tx)
|
||||
|
||||
message.updateWithRecipientAddressStates(
|
||||
guard
|
||||
let deleteMessage = TSInteraction.buildDeleteMessage(
|
||||
thread: thread,
|
||||
message: message,
|
||||
localIdentifiers: localIdentifiers,
|
||||
canAdminDelete: canAdminDelete,
|
||||
tx: tx,
|
||||
)
|
||||
else {
|
||||
return owsFailDebug("Failure to build outgoing delete for everyone.")
|
||||
}
|
||||
|
||||
// TODO: pending send state for incoming messages
|
||||
(message as? TSOutgoingMessage)?.updateWithRecipientAddressStates(
|
||||
deleteMessage.recipientAddressStates,
|
||||
tx: tx,
|
||||
)
|
||||
|
||||
_ = TSMessage.tryToRemotelyDeleteMessage(
|
||||
fromAuthor: aci,
|
||||
sentAtTimestamp: message.timestamp,
|
||||
threadUniqueId: latestThread.uniqueId,
|
||||
serverTimestamp: 0, // TSOutgoingMessage won't have server timestamp.
|
||||
transaction: tx,
|
||||
)
|
||||
if message.canBeRemotelyDeletedByNonAdmin {
|
||||
_ = TSMessage.tryToRemotelyDeleteMessageAsNonAdmin(
|
||||
fromAuthor: localIdentifiers.aci,
|
||||
sentAtTimestamp: message.timestamp,
|
||||
threadUniqueId: latestThread.uniqueId,
|
||||
serverTimestamp: 0, // TSOutgoingMessage won't have server timestamp.
|
||||
transaction: tx,
|
||||
)
|
||||
} else if
|
||||
canAdminDelete,
|
||||
let groupThread = thread as? TSGroupThread
|
||||
{
|
||||
let originalMessageAuthorAci: Aci?
|
||||
let serverTimestamp: UInt64
|
||||
if let incomingMessage = (message as? TSIncomingMessage) {
|
||||
serverTimestamp = incomingMessage.serverTimestamp?.uint64Value ?? 0
|
||||
originalMessageAuthorAci = incomingMessage.authorAddress.aci
|
||||
} else {
|
||||
originalMessageAuthorAci = localIdentifiers.aci
|
||||
serverTimestamp = 0
|
||||
}
|
||||
|
||||
guard let originalMessageAuthorAci else {
|
||||
owsFailDebug("Unable to admin delete without original message author")
|
||||
return
|
||||
}
|
||||
|
||||
_ = DependenciesBridge.shared.adminDeleteManager.tryToAdminDeleteMessage(
|
||||
originalMessageAuthorAci: originalMessageAuthorAci,
|
||||
deleteAuthorAci: localIdentifiers.aci,
|
||||
sentAtTimestamp: message.timestamp,
|
||||
groupThread: groupThread,
|
||||
threadUniqueId: latestThread.uniqueId,
|
||||
serverTimestamp: serverTimestamp,
|
||||
transaction: tx,
|
||||
)
|
||||
} else {
|
||||
owsFailDebug("Unable to delete as admin or as non-admin")
|
||||
return
|
||||
}
|
||||
|
||||
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
||||
transientMessageWithoutAttachments: deleteMessage,
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
import Foundation
|
||||
public import SignalServiceKit
|
||||
import LibSignalClient
|
||||
import SignalUI
|
||||
import UIKit
|
||||
|
||||
@ -88,6 +89,134 @@ public extension TSInteraction {
|
||||
fromViewController.presentActionSheet(actionSheet)
|
||||
}
|
||||
|
||||
/// If the local user is an admin, we prefer regular remote delete. We only fallback to
|
||||
/// admin delete if the remote delete timeframe has expired and admin delete timeframe has not.
|
||||
class func buildDeleteMessage(
|
||||
thread: TSThread,
|
||||
message: TSMessage,
|
||||
localIdentifiers: LocalIdentifiers,
|
||||
canAdminDelete: Bool,
|
||||
tx: DBReadTransaction,
|
||||
) -> TransientOutgoingMessage? {
|
||||
if
|
||||
message.canBeRemotelyDeletedByNonAdmin,
|
||||
let outgoingMessage = message as? TSOutgoingMessage
|
||||
{
|
||||
return OutgoingDeleteMessage(thread: thread, message: outgoingMessage, tx: tx)
|
||||
}
|
||||
|
||||
guard canAdminDelete else {
|
||||
owsFailDebug("Unable to admin-delete incoming message")
|
||||
return nil
|
||||
}
|
||||
|
||||
return OutgoingAdminDeleteMessage(
|
||||
thread: thread,
|
||||
message: message,
|
||||
localIdentifiers: localIdentifiers,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
|
||||
private func buildDeleteForEveryoneAction(thread: TSThread) -> ActionSheetAction? {
|
||||
let adminDeleteManager = DependenciesBridge.shared.adminDeleteManager
|
||||
let db = DependenciesBridge.shared.db
|
||||
|
||||
guard let message = self as? TSMessage else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let canAdminDelete = db.read { tx in adminDeleteManager.canAdminDeleteMessage(message: message, thread: thread, tx: tx) }
|
||||
if message.canBeRemotelyDeletedByNonAdmin || canAdminDelete {
|
||||
return ActionSheetAction(
|
||||
title: CommonStrings.deleteForEveryoneButton,
|
||||
style: .destructive,
|
||||
) { [weak self] _ in
|
||||
guard self != nil else { return }
|
||||
Self.showDeleteForEveryoneConfirmationIfNecessary {
|
||||
SSKEnvironment.shared.databaseStorageRef.write { tx in
|
||||
let latestMessage = TSMessage.fetchMessageViaCache(
|
||||
uniqueId: message.uniqueId,
|
||||
transaction: tx,
|
||||
)
|
||||
guard let latestMessage, let latestThread = latestMessage.thread(tx: tx) else {
|
||||
// We can't reach this point in the UI if a message doesn't have a thread.
|
||||
return owsFailDebug("Trying to delete a message without a thread.")
|
||||
}
|
||||
guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx) else {
|
||||
return owsFailDebug("LocalIdentifiers missing during message deletion.")
|
||||
}
|
||||
|
||||
guard
|
||||
let deleteMessage = Self.buildDeleteMessage(
|
||||
thread: latestThread,
|
||||
message: latestMessage,
|
||||
localIdentifiers: localIdentifiers,
|
||||
canAdminDelete: canAdminDelete,
|
||||
tx: tx,
|
||||
)
|
||||
else {
|
||||
return owsFailDebug("Failure to build outgoing delete for everyone.")
|
||||
}
|
||||
// Reset the sending states, so we can render the sending state of the
|
||||
// deleted message. OutgoingDeleteMessage will automatically pass through
|
||||
// it's send state to the message record that it is deleting.
|
||||
// TODO: support sending state animation for incoming messages.
|
||||
(latestMessage as? TSOutgoingMessage)?.updateWithRecipientAddressStates(deleteMessage.recipientAddressStates, tx: tx)
|
||||
|
||||
if message.canBeRemotelyDeletedByNonAdmin {
|
||||
_ = TSMessage.tryToRemotelyDeleteMessageAsNonAdmin(
|
||||
fromAuthor: localIdentifiers.aci,
|
||||
sentAtTimestamp: latestMessage.timestamp,
|
||||
threadUniqueId: latestThread.uniqueId,
|
||||
serverTimestamp: 0, // TSOutgoingMessage won't have server timestamp.
|
||||
transaction: tx,
|
||||
)
|
||||
} else if
|
||||
canAdminDelete,
|
||||
let groupThread = thread as? TSGroupThread
|
||||
{
|
||||
let originalMessageAuthorAci: Aci?
|
||||
let serverTimestamp: UInt64
|
||||
if let incomingMessage = (latestMessage as? TSIncomingMessage) {
|
||||
serverTimestamp = incomingMessage.serverTimestamp?.uint64Value ?? 0
|
||||
originalMessageAuthorAci = incomingMessage.authorAddress.aci
|
||||
} else {
|
||||
originalMessageAuthorAci = localIdentifiers.aci
|
||||
serverTimestamp = 0
|
||||
}
|
||||
|
||||
guard let originalMessageAuthorAci else {
|
||||
owsFailDebug("Unable to admin delete without original message author")
|
||||
return
|
||||
}
|
||||
|
||||
_ = DependenciesBridge.shared.adminDeleteManager.tryToAdminDeleteMessage(
|
||||
originalMessageAuthorAci: originalMessageAuthorAci,
|
||||
deleteAuthorAci: localIdentifiers.aci,
|
||||
sentAtTimestamp: latestMessage.timestamp,
|
||||
groupThread: groupThread,
|
||||
threadUniqueId: latestThread.uniqueId,
|
||||
serverTimestamp: serverTimestamp,
|
||||
transaction: tx,
|
||||
)
|
||||
} else {
|
||||
owsFailDebug("Unable to delete as admin or as non-admin")
|
||||
return
|
||||
}
|
||||
|
||||
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
||||
transientMessageWithoutAttachments: deleteMessage,
|
||||
)
|
||||
|
||||
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func presentDeletionActionSheetForNotNoteToSelf(
|
||||
fromViewController: UIViewController,
|
||||
thread: TSThread,
|
||||
@ -108,54 +237,7 @@ public extension TSInteraction {
|
||||
thread: thread,
|
||||
))
|
||||
|
||||
if
|
||||
let outgoingMessage = self as? TSOutgoingMessage,
|
||||
outgoingMessage.canBeRemotelyDeleted
|
||||
{
|
||||
let deleteForEveryoneAction = ActionSheetAction(
|
||||
title: CommonStrings.deleteForEveryoneButton,
|
||||
style: .destructive,
|
||||
) { [weak self] _ in
|
||||
guard self != nil else { return }
|
||||
Self.showDeleteForEveryoneConfirmationIfNecessary {
|
||||
SSKEnvironment.shared.databaseStorageRef.write { tx in
|
||||
let latestMessage = TSOutgoingMessage.fetchOutgoingMessageViaCache(
|
||||
uniqueId: outgoingMessage.uniqueId,
|
||||
transaction: tx,
|
||||
)
|
||||
guard let latestMessage, let latestThread = latestMessage.thread(tx: tx) else {
|
||||
// We can't reach this point in the UI if a message doesn't have a thread.
|
||||
return owsFailDebug("Trying to delete a message without a thread.")
|
||||
}
|
||||
let deleteMessage = OutgoingDeleteMessage(
|
||||
thread: latestThread,
|
||||
message: latestMessage,
|
||||
tx: tx,
|
||||
)
|
||||
// Reset the sending states, so we can render the sending state of the
|
||||
// deleted message. OutgoingDeleteMessage will automatically pass through
|
||||
// it's send state to the message record that it is deleting.
|
||||
latestMessage.updateWithRecipientAddressStates(deleteMessage.recipientAddressStates, tx: tx)
|
||||
|
||||
if let aci = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx)?.aci {
|
||||
_ = TSMessage.tryToRemotelyDeleteMessage(
|
||||
fromAuthor: aci,
|
||||
sentAtTimestamp: latestMessage.timestamp,
|
||||
threadUniqueId: latestThread.uniqueId,
|
||||
serverTimestamp: 0, // TSOutgoingMessage won't have server timestamp.
|
||||
transaction: tx,
|
||||
)
|
||||
} else {
|
||||
owsFailDebug("Local ACI missing during message deletion.")
|
||||
}
|
||||
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
||||
transientMessageWithoutAttachments: deleteMessage,
|
||||
)
|
||||
|
||||
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let deleteForEveryoneAction = buildDeleteForEveryoneAction(thread: thread) {
|
||||
actionSheetController.addAction(deleteForEveryoneAction)
|
||||
}
|
||||
|
||||
|
||||
@ -2746,6 +2746,9 @@
|
||||
/* Subheader for an action sheet explaining that a Note to Self deleted on this device will be deleted on the user's other devices as well. */
|
||||
"DELETE_FOR_ME_NOTE_TO_SELF_LINKED_DEVICES_PRESENT_ACTION_SHEET_SUBHEADER" = "This message will be deleted from all your devices.";
|
||||
|
||||
/* Text indicating the message was remotely deleted by an admin. Embeds {{admin display name}} */
|
||||
"DELETED_BY_ADMIN" = "Admin %@ deleted this message";
|
||||
|
||||
/* Label indicating a user who deleted their account. */
|
||||
"DELETED_USER" = "Deleted Account";
|
||||
|
||||
|
||||
@ -1649,10 +1649,16 @@ extension AppSetup.GlobalsContinuation {
|
||||
tsAccountManager: tsAccountManager,
|
||||
)
|
||||
|
||||
let adminDeleteManager = AdminDeleteManager(
|
||||
recipientDatabaseTable: recipientDatabaseTable,
|
||||
tsAccountManager: tsAccountManager,
|
||||
)
|
||||
|
||||
let dependenciesBridge = DependenciesBridge(
|
||||
accountAttributesUpdater: accountAttributesUpdater,
|
||||
accountEntropyPoolManager: accountEntropyPoolManager,
|
||||
adHocCallRecordManager: adHocCallRecordManager,
|
||||
adminDeleteManager: adminDeleteManager,
|
||||
appExpiry: appExpiry,
|
||||
attachmentContentValidator: attachmentContentValidator,
|
||||
attachmentDownloadManager: attachmentDownloadManager,
|
||||
|
||||
@ -90,6 +90,11 @@ public enum BuildFlags {
|
||||
}
|
||||
|
||||
public static let pollOneOnOneSend = build <= .internal
|
||||
|
||||
public enum AdminDelete {
|
||||
public static let receive = build <= .dev
|
||||
public static let send = build <= .dev
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@ -49,6 +49,7 @@ public class DependenciesBridge {
|
||||
public let accountAttributesUpdater: AccountAttributesUpdater
|
||||
public let accountEntropyPoolManager: AccountEntropyPoolManager
|
||||
public let adHocCallRecordManager: any AdHocCallRecordManager
|
||||
public let adminDeleteManager: AdminDeleteManager
|
||||
public let appExpiry: AppExpiry
|
||||
public let attachmentContentValidator: AttachmentContentValidator
|
||||
public let attachmentDownloadManager: AttachmentDownloadManager
|
||||
@ -190,6 +191,7 @@ public class DependenciesBridge {
|
||||
accountAttributesUpdater: AccountAttributesUpdater,
|
||||
accountEntropyPoolManager: AccountEntropyPoolManager,
|
||||
adHocCallRecordManager: any AdHocCallRecordManager,
|
||||
adminDeleteManager: AdminDeleteManager,
|
||||
appExpiry: AppExpiry,
|
||||
attachmentContentValidator: AttachmentContentValidator,
|
||||
attachmentDownloadManager: AttachmentDownloadManager,
|
||||
@ -330,6 +332,7 @@ public class DependenciesBridge {
|
||||
self.accountAttributesUpdater = accountAttributesUpdater
|
||||
self.accountEntropyPoolManager = accountEntropyPoolManager
|
||||
self.adHocCallRecordManager = adHocCallRecordManager
|
||||
self.adminDeleteManager = adminDeleteManager
|
||||
self.appExpiry = appExpiry
|
||||
self.attachmentContentValidator = attachmentContentValidator
|
||||
self.attachmentDownloadManager = attachmentDownloadManager
|
||||
|
||||
@ -0,0 +1,157 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
public import LibSignalClient
|
||||
import GRDB
|
||||
|
||||
public class AdminDeleteManager {
|
||||
let recipientDatabaseTable: RecipientDatabaseTable
|
||||
let tsAccountManager: TSAccountManager
|
||||
|
||||
private let logger = PrefixedLogger(prefix: "AdminDelete")
|
||||
|
||||
init(
|
||||
recipientDatabaseTable: RecipientDatabaseTable,
|
||||
tsAccountManager: TSAccountManager,
|
||||
) {
|
||||
self.recipientDatabaseTable = recipientDatabaseTable
|
||||
self.tsAccountManager = tsAccountManager
|
||||
}
|
||||
|
||||
public enum AdminDeleteProcessingResult: Error {
|
||||
case deletedMessageMissing
|
||||
case invalidDelete
|
||||
case success
|
||||
}
|
||||
|
||||
private func insertAdminDelete(
|
||||
groupThread: TSGroupThread,
|
||||
interactionId: Int64,
|
||||
deleteAuthor: Aci,
|
||||
tx: DBWriteTransaction,
|
||||
) -> AdminDeleteProcessingResult {
|
||||
guard
|
||||
let groupModel = groupThread.groupModel as? TSGroupModelV2,
|
||||
groupModel.membership.isFullMemberAndAdministrator(deleteAuthor)
|
||||
else {
|
||||
logger.error("Failed to process admin delete for non-admin")
|
||||
return .invalidDelete
|
||||
}
|
||||
|
||||
guard
|
||||
let deleteAuthorId = recipientDatabaseTable.fetchRecipient(
|
||||
serviceId: deleteAuthor,
|
||||
transaction: tx,
|
||||
)?.id
|
||||
else {
|
||||
logger.error("Failed to process admin delete for missing signal recipient")
|
||||
return .invalidDelete
|
||||
}
|
||||
|
||||
failIfThrows {
|
||||
try AdminDeleteRecord.insertRecord(
|
||||
interactionId: interactionId,
|
||||
deleteAuthorId: deleteAuthorId,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
return .success
|
||||
}
|
||||
|
||||
public func tryToAdminDeleteMessage(
|
||||
originalMessageAuthorAci: Aci,
|
||||
deleteAuthorAci: Aci,
|
||||
sentAtTimestamp: UInt64,
|
||||
groupThread: TSGroupThread,
|
||||
threadUniqueId: String?,
|
||||
serverTimestamp: UInt64,
|
||||
transaction: DBWriteTransaction,
|
||||
) -> AdminDeleteProcessingResult {
|
||||
guard SDS.fitsInInt64(sentAtTimestamp) else {
|
||||
owsFailDebug("Unable to delete a message with invalid sentAtTimestamp: \(sentAtTimestamp)")
|
||||
return .invalidDelete
|
||||
}
|
||||
|
||||
if
|
||||
let threadUniqueId, let messageToDelete = InteractionFinder.findMessage(
|
||||
withTimestamp: sentAtTimestamp,
|
||||
threadId: threadUniqueId,
|
||||
author: SignalServiceAddress(originalMessageAuthorAci),
|
||||
transaction: transaction,
|
||||
)
|
||||
{
|
||||
let success = TSMessage.remotelyDeleteMessage(
|
||||
messageToDelete,
|
||||
authorAci: originalMessageAuthorAci,
|
||||
isAdminDelete: true,
|
||||
serverTimestamp: serverTimestamp,
|
||||
transaction: transaction,
|
||||
)
|
||||
|
||||
guard success else {
|
||||
return .invalidDelete
|
||||
}
|
||||
|
||||
return insertAdminDelete(
|
||||
groupThread: groupThread,
|
||||
interactionId: messageToDelete.sqliteRowId!,
|
||||
deleteAuthor: deleteAuthorAci,
|
||||
tx: transaction,
|
||||
)
|
||||
} else {
|
||||
return .deletedMessageMissing
|
||||
}
|
||||
}
|
||||
|
||||
public func adminDeleteAuthor(interactionId: Int64, tx: DBReadTransaction) -> Aci? {
|
||||
guard BuildFlags.AdminDelete.receive else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return failIfThrows {
|
||||
guard
|
||||
let adminDeleteRecord = try AdminDeleteRecord
|
||||
.filter(AdminDeleteRecord.Columns.interactionId == interactionId)
|
||||
.fetchOne(tx.database)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let signalRecipient = recipientDatabaseTable.fetchRecipient(
|
||||
rowId: adminDeleteRecord.deleteAuthorId,
|
||||
tx: tx,
|
||||
)
|
||||
return signalRecipient?.aci
|
||||
}
|
||||
}
|
||||
|
||||
public func canAdminDeleteMessage(
|
||||
message: TSMessage,
|
||||
thread: TSThread,
|
||||
tx: DBReadTransaction,
|
||||
) -> Bool {
|
||||
guard BuildFlags.AdminDelete.send else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let groupThread = thread as? TSGroupThread else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let localAci = tsAccountManager.localIdentifiers(tx: tx)?.aci else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard groupThread.groupModel.groupMembership.isFullMemberAndAdministrator(localAci) else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard message.canBeRemotelyDeletedByAdmin else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
public import LibSignalClient
|
||||
|
||||
public final class OutgoingAdminDeleteMessage: TransientOutgoingMessage {
|
||||
let originalMessageTimestamp: UInt64
|
||||
let originalMessageAuthor: Aci?
|
||||
let originalMessageUniqueId: String
|
||||
|
||||
public init(
|
||||
thread: TSThread,
|
||||
message: TSMessage,
|
||||
localIdentifiers: LocalIdentifiers,
|
||||
tx: DBReadTransaction,
|
||||
) {
|
||||
owsAssertDebug(thread.uniqueId == message.uniqueThreadId)
|
||||
|
||||
self.originalMessageTimestamp = message.timestamp
|
||||
|
||||
if let incomingMessage = message as? TSIncomingMessage {
|
||||
self.originalMessageAuthor = incomingMessage.authorAddress.aci
|
||||
} else {
|
||||
self.originalMessageAuthor = localIdentifiers.aci
|
||||
}
|
||||
|
||||
self.originalMessageUniqueId = message.uniqueId
|
||||
|
||||
super.init(
|
||||
outgoingMessageWith: TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread),
|
||||
additionalRecipients: [],
|
||||
explicitRecipients: [],
|
||||
skippedRecipients: [],
|
||||
transaction: tx,
|
||||
)
|
||||
}
|
||||
|
||||
override public class var supportsSecureCoding: Bool { true }
|
||||
|
||||
override public func encode(with coder: NSCoder) {
|
||||
super.encode(with: coder)
|
||||
coder.encode(NSNumber(value: self.originalMessageTimestamp), forKey: "originalMessageTimestamp")
|
||||
if let originalMessageAuthor {
|
||||
coder.encode(originalMessageAuthor.serviceIdBinary, forKey: "originalMessageAuthorBinary")
|
||||
}
|
||||
coder.encode(originalMessageUniqueId, forKey: "originalMessageUniqueId")
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
guard
|
||||
let originalMessageTimestamp = coder.decodeObject(of: NSNumber.self, forKey: "originalMessageTimestamp"),
|
||||
let originalMessageAuthorBinary = coder.decodeObject(of: NSData.self, forKey: "originalMessageAuthorBinary") as Data?,
|
||||
let originalMessageUniqueId = coder.decodeObject(of: NSString.self, forKey: "originalMessageUniqueId") as String?
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
self.originalMessageTimestamp = originalMessageTimestamp.uint64Value
|
||||
self.originalMessageAuthor = try? Aci.parseFrom(serviceIdBinary: originalMessageAuthorBinary)
|
||||
self.originalMessageUniqueId = originalMessageUniqueId
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
override public var hash: Int {
|
||||
var hasher = Hasher()
|
||||
hasher.combine(super.hash)
|
||||
hasher.combine(self.originalMessageTimestamp)
|
||||
hasher.combine(self.originalMessageAuthor)
|
||||
hasher.combine(self.originalMessageUniqueId)
|
||||
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.originalMessageTimestamp == object.originalMessageTimestamp else { return false }
|
||||
guard self.originalMessageAuthor == object.originalMessageAuthor else { return false }
|
||||
guard self.originalMessageUniqueId == object.originalMessageUniqueId else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
override public func dataMessageBuilder(with thread: TSThread, transaction: DBReadTransaction) -> SSKProtoDataMessageBuilder? {
|
||||
guard let originalMessageAuthor else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let adminDeleteBuilder = SSKProtoDataMessageAdminDelete.builder()
|
||||
adminDeleteBuilder.setTargetAuthorAciBinary(originalMessageAuthor.serviceIdBinary)
|
||||
adminDeleteBuilder.setTargetSentTimestamp(originalMessageTimestamp)
|
||||
|
||||
let builder = super.dataMessageBuilder(with: thread, transaction: transaction)
|
||||
builder?.setTimestamp(self.timestamp)
|
||||
builder?.setAdminDelete(adminDeleteBuilder.buildInfallibly())
|
||||
return builder
|
||||
}
|
||||
|
||||
override public func anyUpdateOutgoingMessage(transaction: DBWriteTransaction, block: (TSOutgoingMessage) -> Void) {
|
||||
super.anyUpdateOutgoingMessage(transaction: transaction, block: block)
|
||||
|
||||
let deletedMessage = TSMessage.fetchMessageViaCache(
|
||||
uniqueId: originalMessageUniqueId,
|
||||
transaction: transaction,
|
||||
)
|
||||
if let outgoingDeletedMessage = deletedMessage as? TSOutgoingMessage {
|
||||
outgoingDeletedMessage.updateWithRecipientAddressStates(self.recipientAddressStates, tx: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
override public var relatedUniqueIds: Set<String> {
|
||||
return super.relatedUniqueIds.union([self.originalMessageUniqueId].compacted())
|
||||
}
|
||||
|
||||
// TODO: revert local state if send to everyone fails
|
||||
}
|
||||
@ -311,12 +311,23 @@ public extension TSMessage {
|
||||
// * it's not a message with a gift badge
|
||||
// * it has been less than 24 hours since you sent the message
|
||||
// * this includes messages sent in the future
|
||||
var canBeRemotelyDeleted: Bool {
|
||||
var canBeRemotelyDeletedByNonAdmin: Bool {
|
||||
guard let outgoingMessage = self as? TSOutgoingMessage else { return false }
|
||||
guard !outgoingMessage.wasRemotelyDeleted else { return false }
|
||||
guard outgoingMessage.giftBadge == nil else { return false }
|
||||
|
||||
let (elapsedTime, isInFuture) = Date.ows_millisecondTimestamp().subtractingReportingOverflow(outgoingMessage.timestamp)
|
||||
|
||||
// TODO: replace with global config
|
||||
guard isInFuture || (elapsedTime <= UInt64.dayInMs) else { return false }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var canBeRemotelyDeletedByAdmin: Bool {
|
||||
let (elapsedTime, isInFuture) = Date.ows_millisecondTimestamp().subtractingReportingOverflow(self.timestamp)
|
||||
|
||||
// TODO: replace with global config
|
||||
guard isInFuture || (elapsedTime <= UInt64.dayInMs) else { return false }
|
||||
|
||||
return true
|
||||
@ -329,7 +340,67 @@ public extension TSMessage {
|
||||
case success
|
||||
}
|
||||
|
||||
class func tryToRemotelyDeleteMessage(
|
||||
static func remotelyDeleteMessage(
|
||||
_ message: TSMessage,
|
||||
authorAci: Aci,
|
||||
isAdminDelete: Bool,
|
||||
serverTimestamp: UInt64,
|
||||
transaction: DBWriteTransaction,
|
||||
) -> Bool {
|
||||
if message is TSOutgoingMessage {
|
||||
if SignalServiceAddress(authorAci).isLocalAddress || isAdminDelete {
|
||||
message.markMessageAsRemotelyDeleted(transaction: transaction)
|
||||
return true
|
||||
}
|
||||
owsFailDebug("Can't delete an outgoing message by non-local user unless its an admin delete.")
|
||||
return false
|
||||
} else if var incomingMessageToDelete = message as? TSIncomingMessage {
|
||||
if incomingMessageToDelete.editState == .pastRevision {
|
||||
// The remote delete targeted an old revision, fetch
|
||||
// swap out the target message for the latest (or return an error)
|
||||
// This avoids cases where older edits could be deleted and
|
||||
// leave newer revisions
|
||||
if
|
||||
let latestEdit = DependenciesBridge.shared.editMessageStore.findMessage(
|
||||
fromEdit: incomingMessageToDelete,
|
||||
tx: transaction,
|
||||
) as? TSIncomingMessage
|
||||
{
|
||||
incomingMessageToDelete = latestEdit
|
||||
} else {
|
||||
Logger.info("Ignoring delete for missing edit target.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
guard let messageToDeleteServerTimestamp = incomingMessageToDelete.serverTimestamp?.uint64Value else {
|
||||
// Older messages might be missing this, but since we only allow deleting for a small
|
||||
// window after you send a message we should generally never hit this path.
|
||||
owsFailDebug("can't delete a message without a serverTimestamp")
|
||||
return false
|
||||
}
|
||||
|
||||
guard messageToDeleteServerTimestamp <= serverTimestamp else {
|
||||
owsFailDebug("Can't delete a message from the future.")
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: This should eventually be determined via global remote config separated by admin vs regular remote delete.
|
||||
guard serverTimestamp - messageToDeleteServerTimestamp < (2 * UInt64.dayInMs) else {
|
||||
owsFailDebug("Ignoring message delete sent more than 48 hours after the original message")
|
||||
return false
|
||||
}
|
||||
|
||||
incomingMessageToDelete.markMessageAsRemotelyDeleted(transaction: transaction)
|
||||
|
||||
return true
|
||||
} else {
|
||||
owsFailDebug("Message to delete is not incoming or outgoing")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class func tryToRemotelyDeleteMessageAsNonAdmin(
|
||||
fromAuthor authorAci: Aci,
|
||||
sentAtTimestamp: UInt64,
|
||||
threadUniqueId: String?,
|
||||
@ -349,52 +420,15 @@ public extension TSMessage {
|
||||
transaction: transaction,
|
||||
)
|
||||
{
|
||||
if messageToDelete is TSOutgoingMessage, SignalServiceAddress(authorAci).isLocalAddress {
|
||||
messageToDelete.markMessageAsRemotelyDeleted(transaction: transaction)
|
||||
return .success
|
||||
} else if var incomingMessageToDelete = messageToDelete as? TSIncomingMessage {
|
||||
if incomingMessageToDelete.editState == .pastRevision {
|
||||
// The remote delete targeted an old revision, fetch
|
||||
// swap out the target message for the latest (or return an error)
|
||||
// This avoids cases where older edits could be deleted and
|
||||
// leave newer revisions
|
||||
if
|
||||
let latestEdit = DependenciesBridge.shared.editMessageStore.findMessage(
|
||||
fromEdit: incomingMessageToDelete,
|
||||
tx: transaction,
|
||||
) as? TSIncomingMessage
|
||||
{
|
||||
incomingMessageToDelete = latestEdit
|
||||
} else {
|
||||
Logger.info("Ignoring delete for missing edit target.")
|
||||
return .invalidDelete
|
||||
}
|
||||
}
|
||||
let success = remotelyDeleteMessage(
|
||||
messageToDelete,
|
||||
authorAci: authorAci,
|
||||
isAdminDelete: false,
|
||||
serverTimestamp: serverTimestamp,
|
||||
transaction: transaction,
|
||||
)
|
||||
|
||||
guard let messageToDeleteServerTimestamp = incomingMessageToDelete.serverTimestamp?.uint64Value else {
|
||||
// Older messages might be missing this, but since we only allow deleting for a small
|
||||
// window after you send a message we should generally never hit this path.
|
||||
owsFailDebug("can't delete a message without a serverTimestamp")
|
||||
return .invalidDelete
|
||||
}
|
||||
|
||||
guard messageToDeleteServerTimestamp < serverTimestamp else {
|
||||
owsFailDebug("Can't delete a message from the future.")
|
||||
return .invalidDelete
|
||||
}
|
||||
|
||||
guard serverTimestamp - messageToDeleteServerTimestamp < (2 * UInt64.dayInMs) else {
|
||||
owsFailDebug("Ignoring message delete sent more than 48 hours after the original message")
|
||||
return .invalidDelete
|
||||
}
|
||||
|
||||
incomingMessageToDelete.markMessageAsRemotelyDeleted(transaction: transaction)
|
||||
|
||||
return .success
|
||||
} else {
|
||||
owsFailDebug("Only incoming messages can be deleted remotely")
|
||||
return .invalidDelete
|
||||
}
|
||||
return success ? .success : .invalidDelete
|
||||
} else if
|
||||
let storyMessage = StoryFinder.story(
|
||||
timestamp: sentAtTimestamp,
|
||||
|
||||
@ -468,7 +468,7 @@ public final class MessageReceiver {
|
||||
)
|
||||
}
|
||||
} else if let delete = dataMessage.delete {
|
||||
let result = TSMessage.tryToRemotelyDeleteMessage(
|
||||
let result = TSMessage.tryToRemotelyDeleteMessageAsNonAdmin(
|
||||
fromAuthor: decryptedEnvelope.sourceAci,
|
||||
sentAtTimestamp: delete.targetSentTimestamp,
|
||||
threadUniqueId: transcript.threadForDataMessage?.uniqueId,
|
||||
@ -491,6 +491,52 @@ public final class MessageReceiver {
|
||||
transaction: tx,
|
||||
)
|
||||
}
|
||||
} else if let adminDelete = dataMessage.adminDelete {
|
||||
guard BuildFlags.AdminDelete.receive else {
|
||||
Logger.warn("Dropping admin delete message because build flag is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
let adminDeleteManager = DependenciesBridge.shared.adminDeleteManager
|
||||
let earlyMessageManager = SSKEnvironment.shared.earlyMessageManagerRef
|
||||
guard let groupThread = transcript.threadForDataMessage as? TSGroupThread else {
|
||||
owsFailDebug("Could not process admin delete thread from sync transcript.")
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
let targetAuthorAciBinary = adminDelete.targetAuthorAciBinary,
|
||||
let targetAuthorAci = try? Aci.parseFrom(serviceIdBinary: targetAuthorAciBinary)
|
||||
else {
|
||||
Logger.error("Couldn't process admin delete for invalid aci")
|
||||
return
|
||||
}
|
||||
let result = adminDeleteManager.tryToAdminDeleteMessage(
|
||||
originalMessageAuthorAci: targetAuthorAci,
|
||||
deleteAuthorAci: localIdentifiers.aci,
|
||||
sentAtTimestamp: adminDelete.targetSentTimestamp,
|
||||
groupThread: groupThread,
|
||||
threadUniqueId: groupThread.uniqueId,
|
||||
serverTimestamp: envelope.serverTimestamp,
|
||||
transaction: tx,
|
||||
)
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .invalidDelete:
|
||||
Logger.warn("Couldn't process invalid admin delete")
|
||||
case .deletedMessageMissing:
|
||||
earlyMessageManager.recordEarlyEnvelope(
|
||||
envelope,
|
||||
plainTextData: request.plaintextData,
|
||||
wasReceivedByUD: request.wasReceivedByUD,
|
||||
serverDeliveryTimestamp: request.serverDeliveryTimestamp,
|
||||
associatedMessageTimestamp: adminDelete.targetSentTimestamp,
|
||||
associatedMessageAuthor: decryptedEnvelope.sourceAci,
|
||||
transaction: tx,
|
||||
)
|
||||
}
|
||||
} else if let groupCallUpdate = dataMessage.groupCallUpdate {
|
||||
if let groupId {
|
||||
let pendingTask = MessageReceiver.buildPendingTask()
|
||||
@ -1081,7 +1127,7 @@ public final class MessageReceiver {
|
||||
}
|
||||
|
||||
if let delete = dataMessage.delete {
|
||||
let result = TSMessage.tryToRemotelyDeleteMessage(
|
||||
let result = TSMessage.tryToRemotelyDeleteMessageAsNonAdmin(
|
||||
fromAuthor: envelope.sourceAci,
|
||||
sentAtTimestamp: delete.targetSentTimestamp,
|
||||
threadUniqueId: thread.uniqueId,
|
||||
@ -1107,6 +1153,54 @@ public final class MessageReceiver {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let adminDelete = dataMessage.adminDelete {
|
||||
guard BuildFlags.AdminDelete.receive else {
|
||||
Logger.warn("Dropping admin delete message because build flag is not enabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
let adminDeleteManager = DependenciesBridge.shared.adminDeleteManager
|
||||
guard let groupThread = (thread as? TSGroupThread) else {
|
||||
Logger.error("Couldn't process admin delete for non-group thread")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard
|
||||
let targetAuthorAciBinary = adminDelete.targetAuthorAciBinary,
|
||||
let targetAuthorAci = try? Aci.parseFrom(serviceIdBinary: targetAuthorAciBinary)
|
||||
else {
|
||||
Logger.error("Couldn't process admin delete for invalid aci")
|
||||
return nil
|
||||
}
|
||||
let result = adminDeleteManager.tryToAdminDeleteMessage(
|
||||
originalMessageAuthorAci: targetAuthorAci,
|
||||
deleteAuthorAci: envelope.sourceAci,
|
||||
sentAtTimestamp: adminDelete.targetSentTimestamp,
|
||||
groupThread: groupThread,
|
||||
threadUniqueId: thread.uniqueId,
|
||||
serverTimestamp: envelope.serverTimestamp,
|
||||
transaction: tx,
|
||||
)
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .invalidDelete:
|
||||
Logger.warn("Couldn't process invalid admin delete")
|
||||
case .deletedMessageMissing:
|
||||
SSKEnvironment.shared.earlyMessageManagerRef.recordEarlyEnvelope(
|
||||
envelope.envelope,
|
||||
plainTextData: request.plaintextData,
|
||||
wasReceivedByUD: request.wasReceivedByUD,
|
||||
serverDeliveryTimestamp: request.serverDeliveryTimestamp,
|
||||
associatedMessageTimestamp: adminDelete.targetSentTimestamp,
|
||||
associatedMessageAuthor: envelope.sourceAci,
|
||||
transaction: tx,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if let pollVote = dataMessage.pollVote {
|
||||
do {
|
||||
guard
|
||||
|
||||
@ -8839,6 +8839,177 @@ extension SSKProtoDataMessageUnpinMessageBuilder {
|
||||
|
||||
#endif
|
||||
|
||||
// MARK: - SSKProtoDataMessageAdminDelete
|
||||
|
||||
@objc
|
||||
public class SSKProtoDataMessageAdminDelete: NSObject, Codable, NSSecureCoding {
|
||||
|
||||
fileprivate let proto: SignalServiceProtos_DataMessage.AdminDelete
|
||||
|
||||
@objc
|
||||
public var targetAuthorAciBinary: Data? {
|
||||
guard hasTargetAuthorAciBinary else {
|
||||
return nil
|
||||
}
|
||||
return proto.targetAuthorAciBinary
|
||||
}
|
||||
@objc
|
||||
public var hasTargetAuthorAciBinary: Bool {
|
||||
return proto.hasTargetAuthorAciBinary
|
||||
}
|
||||
|
||||
@objc
|
||||
public var targetSentTimestamp: UInt64 {
|
||||
return proto.targetSentTimestamp
|
||||
}
|
||||
@objc
|
||||
public var hasTargetSentTimestamp: Bool {
|
||||
return proto.hasTargetSentTimestamp
|
||||
}
|
||||
|
||||
public var hasUnknownFields: Bool {
|
||||
return !proto.unknownFields.data.isEmpty
|
||||
}
|
||||
public var unknownFields: SwiftProtobuf.UnknownStorage? {
|
||||
guard hasUnknownFields else { return nil }
|
||||
return proto.unknownFields
|
||||
}
|
||||
|
||||
private init(proto: SignalServiceProtos_DataMessage.AdminDelete) {
|
||||
self.proto = proto
|
||||
}
|
||||
|
||||
@objc
|
||||
public func serializedData() throws -> Data {
|
||||
return try self.proto.serializedData()
|
||||
}
|
||||
|
||||
@objc
|
||||
public required convenience init(serializedData: Data) throws {
|
||||
let proto = try SignalServiceProtos_DataMessage.AdminDelete(serializedBytes: serializedData)
|
||||
self.init(proto)
|
||||
}
|
||||
|
||||
fileprivate convenience init(_ proto: SignalServiceProtos_DataMessage.AdminDelete) {
|
||||
self.init(proto: proto)
|
||||
}
|
||||
|
||||
public required convenience init(from decoder: Swift.Decoder) throws {
|
||||
let singleValueContainer = try decoder.singleValueContainer()
|
||||
let serializedData = try singleValueContainer.decode(Data.self)
|
||||
try self.init(serializedData: serializedData)
|
||||
}
|
||||
public func encode(to encoder: Swift.Encoder) throws {
|
||||
var singleValueContainer = encoder.singleValueContainer()
|
||||
try singleValueContainer.encode(try serializedData())
|
||||
}
|
||||
|
||||
public static var supportsSecureCoding: Bool { true }
|
||||
|
||||
public required convenience init?(coder: NSCoder) {
|
||||
guard let serializedData = coder.decodeData() else { return nil }
|
||||
do {
|
||||
try self.init(serializedData: serializedData)
|
||||
} catch {
|
||||
owsFailDebug("Failed to decode serialized data \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
do {
|
||||
coder.encode(try serializedData())
|
||||
} catch {
|
||||
owsFailDebug("Failed to encode serialized data \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public override var debugDescription: String {
|
||||
return "\(proto)"
|
||||
}
|
||||
}
|
||||
|
||||
extension SSKProtoDataMessageAdminDelete {
|
||||
@objc
|
||||
public static func builder() -> SSKProtoDataMessageAdminDeleteBuilder {
|
||||
return SSKProtoDataMessageAdminDeleteBuilder()
|
||||
}
|
||||
|
||||
// asBuilder() constructs a builder that reflects the proto's contents.
|
||||
@objc
|
||||
public func asBuilder() -> SSKProtoDataMessageAdminDeleteBuilder {
|
||||
let builder = SSKProtoDataMessageAdminDeleteBuilder()
|
||||
if let _value = targetAuthorAciBinary {
|
||||
builder.setTargetAuthorAciBinary(_value)
|
||||
}
|
||||
if hasTargetSentTimestamp {
|
||||
builder.setTargetSentTimestamp(targetSentTimestamp)
|
||||
}
|
||||
if let _value = unknownFields {
|
||||
builder.setUnknownFields(_value)
|
||||
}
|
||||
return builder
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public class SSKProtoDataMessageAdminDeleteBuilder: NSObject {
|
||||
|
||||
private var proto = SignalServiceProtos_DataMessage.AdminDelete()
|
||||
|
||||
@objc
|
||||
fileprivate override init() {}
|
||||
|
||||
@objc
|
||||
@available(swift, obsoleted: 1.0)
|
||||
public func setTargetAuthorAciBinary(_ valueParam: Data?) {
|
||||
guard let valueParam = valueParam else { return }
|
||||
proto.targetAuthorAciBinary = valueParam
|
||||
}
|
||||
|
||||
public func setTargetAuthorAciBinary(_ valueParam: Data) {
|
||||
proto.targetAuthorAciBinary = valueParam
|
||||
}
|
||||
|
||||
@objc
|
||||
public func setTargetSentTimestamp(_ valueParam: UInt64) {
|
||||
proto.targetSentTimestamp = valueParam
|
||||
}
|
||||
|
||||
public func setUnknownFields(_ unknownFields: SwiftProtobuf.UnknownStorage) {
|
||||
proto.unknownFields = unknownFields
|
||||
}
|
||||
|
||||
@objc
|
||||
public func buildInfallibly() -> SSKProtoDataMessageAdminDelete {
|
||||
return SSKProtoDataMessageAdminDelete(proto)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func buildSerializedData() throws -> Data {
|
||||
return try SSKProtoDataMessageAdminDelete(proto).serializedData()
|
||||
}
|
||||
}
|
||||
|
||||
#if TESTABLE_BUILD
|
||||
|
||||
extension SSKProtoDataMessageAdminDelete {
|
||||
@objc
|
||||
public func serializedDataIgnoringErrors() -> Data? {
|
||||
return try! self.serializedData()
|
||||
}
|
||||
}
|
||||
|
||||
extension SSKProtoDataMessageAdminDeleteBuilder {
|
||||
@objc
|
||||
public func buildIgnoringErrors() -> SSKProtoDataMessageAdminDelete? {
|
||||
return self.buildInfallibly()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// MARK: - SSKProtoDataMessageFlags
|
||||
|
||||
@objc
|
||||
@ -8968,6 +9139,9 @@ public class SSKProtoDataMessage: NSObject, Codable, NSSecureCoding {
|
||||
@objc
|
||||
public let unpinMessage: SSKProtoDataMessageUnpinMessage?
|
||||
|
||||
@objc
|
||||
public let adminDelete: SSKProtoDataMessageAdminDelete?
|
||||
|
||||
@objc
|
||||
public var body: String? {
|
||||
guard hasBody else {
|
||||
@ -9072,7 +9246,8 @@ public class SSKProtoDataMessage: NSObject, Codable, NSSecureCoding {
|
||||
pollTerminate: SSKProtoDataMessagePollTerminate?,
|
||||
pollVote: SSKProtoDataMessagePollVote?,
|
||||
pinMessage: SSKProtoDataMessagePinMessage?,
|
||||
unpinMessage: SSKProtoDataMessageUnpinMessage?) {
|
||||
unpinMessage: SSKProtoDataMessageUnpinMessage?,
|
||||
adminDelete: SSKProtoDataMessageAdminDelete?) {
|
||||
self.proto = proto
|
||||
self.attachments = attachments
|
||||
self.groupV2 = groupV2
|
||||
@ -9092,6 +9267,7 @@ public class SSKProtoDataMessage: NSObject, Codable, NSSecureCoding {
|
||||
self.pollVote = pollVote
|
||||
self.pinMessage = pinMessage
|
||||
self.unpinMessage = unpinMessage
|
||||
self.adminDelete = adminDelete
|
||||
}
|
||||
|
||||
@objc
|
||||
@ -9188,6 +9364,11 @@ public class SSKProtoDataMessage: NSObject, Codable, NSSecureCoding {
|
||||
unpinMessage = SSKProtoDataMessageUnpinMessage(proto.unpinMessage)
|
||||
}
|
||||
|
||||
var adminDelete: SSKProtoDataMessageAdminDelete?
|
||||
if proto.hasAdminDelete {
|
||||
adminDelete = SSKProtoDataMessageAdminDelete(proto.adminDelete)
|
||||
}
|
||||
|
||||
self.init(proto: proto,
|
||||
attachments: attachments,
|
||||
groupV2: groupV2,
|
||||
@ -9206,7 +9387,8 @@ public class SSKProtoDataMessage: NSObject, Codable, NSSecureCoding {
|
||||
pollTerminate: pollTerminate,
|
||||
pollVote: pollVote,
|
||||
pinMessage: pinMessage,
|
||||
unpinMessage: unpinMessage)
|
||||
unpinMessage: unpinMessage,
|
||||
adminDelete: adminDelete)
|
||||
}
|
||||
|
||||
public required convenience init(from decoder: Swift.Decoder) throws {
|
||||
@ -9325,6 +9507,9 @@ extension SSKProtoDataMessage {
|
||||
if let _value = unpinMessage {
|
||||
builder.setUnpinMessage(_value)
|
||||
}
|
||||
if let _value = adminDelete {
|
||||
builder.setAdminDelete(_value)
|
||||
}
|
||||
if let _value = unknownFields {
|
||||
builder.setUnknownFields(_value)
|
||||
}
|
||||
@ -9586,6 +9771,17 @@ public class SSKProtoDataMessageBuilder: NSObject {
|
||||
proto.unpinMessage = valueParam.proto
|
||||
}
|
||||
|
||||
@objc
|
||||
@available(swift, obsoleted: 1.0)
|
||||
public func setAdminDelete(_ valueParam: SSKProtoDataMessageAdminDelete?) {
|
||||
guard let valueParam = valueParam else { return }
|
||||
proto.adminDelete = valueParam.proto
|
||||
}
|
||||
|
||||
public func setAdminDelete(_ valueParam: SSKProtoDataMessageAdminDelete) {
|
||||
proto.adminDelete = valueParam.proto
|
||||
}
|
||||
|
||||
public func setUnknownFields(_ unknownFields: SwiftProtobuf.UnknownStorage) {
|
||||
proto.unknownFields = unknownFields
|
||||
}
|
||||
|
||||
@ -1196,7 +1196,6 @@ struct SignalServiceProtos_DataMessage: @unchecked Sendable {
|
||||
/// Clears the value of `pinMessage`. Subsequent reads from it will return its default value.
|
||||
mutating func clearPinMessage() {_uniqueStorage()._pinMessage = nil}
|
||||
|
||||
/// NEXT ID: 29
|
||||
var unpinMessage: SignalServiceProtos_DataMessage.UnpinMessage {
|
||||
get {return _storage._unpinMessage ?? SignalServiceProtos_DataMessage.UnpinMessage()}
|
||||
set {_uniqueStorage()._unpinMessage = newValue}
|
||||
@ -1206,6 +1205,16 @@ struct SignalServiceProtos_DataMessage: @unchecked Sendable {
|
||||
/// Clears the value of `unpinMessage`. Subsequent reads from it will return its default value.
|
||||
mutating func clearUnpinMessage() {_uniqueStorage()._unpinMessage = nil}
|
||||
|
||||
/// NEXT ID: 30
|
||||
var adminDelete: SignalServiceProtos_DataMessage.AdminDelete {
|
||||
get {return _storage._adminDelete ?? SignalServiceProtos_DataMessage.AdminDelete()}
|
||||
set {_uniqueStorage()._adminDelete = newValue}
|
||||
}
|
||||
/// Returns true if `adminDelete` has been explicitly set.
|
||||
var hasAdminDelete: Bool {return _storage._adminDelete != nil}
|
||||
/// Clears the value of `adminDelete`. Subsequent reads from it will return its default value.
|
||||
mutating func clearAdminDelete() {_uniqueStorage()._adminDelete = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
enum Flags: Int, SwiftProtobuf.Enum, Swift.CaseIterable {
|
||||
@ -2287,6 +2296,38 @@ struct SignalServiceProtos_DataMessage: @unchecked Sendable {
|
||||
fileprivate var _targetSentTimestamp: UInt64? = nil
|
||||
}
|
||||
|
||||
struct AdminDelete: Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
/// 16-byte UUID
|
||||
var targetAuthorAciBinary: Data {
|
||||
get {return _targetAuthorAciBinary ?? Data()}
|
||||
set {_targetAuthorAciBinary = newValue}
|
||||
}
|
||||
/// Returns true if `targetAuthorAciBinary` has been explicitly set.
|
||||
var hasTargetAuthorAciBinary: Bool {return self._targetAuthorAciBinary != nil}
|
||||
/// Clears the value of `targetAuthorAciBinary`. Subsequent reads from it will return its default value.
|
||||
mutating func clearTargetAuthorAciBinary() {self._targetAuthorAciBinary = nil}
|
||||
|
||||
var targetSentTimestamp: UInt64 {
|
||||
get {return _targetSentTimestamp ?? 0}
|
||||
set {_targetSentTimestamp = newValue}
|
||||
}
|
||||
/// Returns true if `targetSentTimestamp` has been explicitly set.
|
||||
var hasTargetSentTimestamp: Bool {return self._targetSentTimestamp != nil}
|
||||
/// Clears the value of `targetSentTimestamp`. Subsequent reads from it will return its default value.
|
||||
mutating func clearTargetSentTimestamp() {self._targetSentTimestamp = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _targetAuthorAciBinary: Data? = nil
|
||||
fileprivate var _targetSentTimestamp: UInt64? = nil
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _storage = _StorageClass.defaultInstance
|
||||
@ -5607,7 +5648,7 @@ extension SignalServiceProtos_CallMessage.Opaque.Urgency: SwiftProtobuf._ProtoNa
|
||||
|
||||
extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".DataMessage"
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}body\0\u{1}attachments\0\u{2}\u{2}flags\0\u{1}expireTimer\0\u{1}profileKey\0\u{1}timestamp\0\u{1}quote\0\u{1}contact\0\u{1}preview\0\u{1}sticker\0\u{1}requiredProtocolVersion\0\u{2}\u{2}isViewOnce\0\u{1}groupV2\0\u{1}reaction\0\u{1}delete\0\u{1}bodyRanges\0\u{1}groupCallUpdate\0\u{1}payment\0\u{1}storyContext\0\u{1}giftBadge\0\u{1}expireTimerVersion\0\u{1}pollCreate\0\u{1}pollTerminate\0\u{1}pollVote\0\u{1}pinMessage\0\u{1}unpinMessage\0\u{c}\u{3}\u{1}")
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}body\0\u{1}attachments\0\u{2}\u{2}flags\0\u{1}expireTimer\0\u{1}profileKey\0\u{1}timestamp\0\u{1}quote\0\u{1}contact\0\u{1}preview\0\u{1}sticker\0\u{1}requiredProtocolVersion\0\u{2}\u{2}isViewOnce\0\u{1}groupV2\0\u{1}reaction\0\u{1}delete\0\u{1}bodyRanges\0\u{1}groupCallUpdate\0\u{1}payment\0\u{1}storyContext\0\u{1}giftBadge\0\u{1}expireTimerVersion\0\u{1}pollCreate\0\u{1}pollTerminate\0\u{1}pollVote\0\u{1}pinMessage\0\u{1}unpinMessage\0\u{1}adminDelete\0\u{c}\u{3}\u{1}")
|
||||
|
||||
fileprivate class _StorageClass {
|
||||
var _body: String? = nil
|
||||
@ -5636,6 +5677,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
|
||||
var _pollVote: SignalServiceProtos_DataMessage.PollVote? = nil
|
||||
var _pinMessage: SignalServiceProtos_DataMessage.PinMessage? = nil
|
||||
var _unpinMessage: SignalServiceProtos_DataMessage.UnpinMessage? = nil
|
||||
var _adminDelete: SignalServiceProtos_DataMessage.AdminDelete? = nil
|
||||
|
||||
// This property is used as the initial default value for new instances of the type.
|
||||
// The type itself is protecting the reference to its storage via CoW semantics.
|
||||
@ -5672,6 +5714,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
|
||||
_pollVote = source._pollVote
|
||||
_pinMessage = source._pinMessage
|
||||
_unpinMessage = source._unpinMessage
|
||||
_adminDelete = source._adminDelete
|
||||
}
|
||||
}
|
||||
|
||||
@ -5716,6 +5759,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
|
||||
case 26: try { try decoder.decodeSingularMessageField(value: &_storage._pollVote) }()
|
||||
case 27: try { try decoder.decodeSingularMessageField(value: &_storage._pinMessage) }()
|
||||
case 28: try { try decoder.decodeSingularMessageField(value: &_storage._unpinMessage) }()
|
||||
case 29: try { try decoder.decodeSingularMessageField(value: &_storage._adminDelete) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
@ -5806,6 +5850,9 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
|
||||
try { if let v = _storage._unpinMessage {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 28)
|
||||
} }()
|
||||
try { if let v = _storage._adminDelete {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 29)
|
||||
} }()
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
@ -5841,6 +5888,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
|
||||
if _storage._pollVote != rhs_storage._pollVote {return false}
|
||||
if _storage._pinMessage != rhs_storage._pinMessage {return false}
|
||||
if _storage._unpinMessage != rhs_storage._unpinMessage {return false}
|
||||
if _storage._adminDelete != rhs_storage._adminDelete {return false}
|
||||
return true
|
||||
}
|
||||
if !storagesAreEqual {return false}
|
||||
@ -7105,6 +7153,45 @@ extension SignalServiceProtos_DataMessage.UnpinMessage: SwiftProtobuf.Message, S
|
||||
}
|
||||
}
|
||||
|
||||
extension SignalServiceProtos_DataMessage.AdminDelete: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = SignalServiceProtos_DataMessage.protoMessageName + ".AdminDelete"
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}targetAuthorAciBinary\0\u{1}targetSentTimestamp\0")
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularBytesField(value: &self._targetAuthorAciBinary) }()
|
||||
case 2: try { try decoder.decodeSingularUInt64Field(value: &self._targetSentTimestamp) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._targetAuthorAciBinary {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
|
||||
} }()
|
||||
try { if let v = self._targetSentTimestamp {
|
||||
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 2)
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: SignalServiceProtos_DataMessage.AdminDelete, rhs: SignalServiceProtos_DataMessage.AdminDelete) -> Bool {
|
||||
if lhs._targetAuthorAciBinary != rhs._targetAuthorAciBinary {return false}
|
||||
if lhs._targetSentTimestamp != rhs._targetSentTimestamp {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension SignalServiceProtos_NullMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".NullMessage"
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}padding\0")
|
||||
|
||||
@ -430,6 +430,11 @@ message DataMessage {
|
||||
optional uint64 targetSentTimestamp = 2;
|
||||
}
|
||||
|
||||
message AdminDelete {
|
||||
optional bytes targetAuthorAciBinary = 1; // 16-byte UUID
|
||||
optional uint64 targetSentTimestamp = 2;
|
||||
}
|
||||
|
||||
optional string body = 1;
|
||||
repeated AttachmentPointer attachments = 2;
|
||||
reserved /*groupV1Context*/ 3;
|
||||
@ -457,7 +462,8 @@ message DataMessage {
|
||||
optional PollVote pollVote = 26;
|
||||
optional PinMessage pinMessage = 27;
|
||||
optional UnpinMessage unpinMessage = 28;
|
||||
// NEXT ID: 29
|
||||
optional AdminDelete adminDelete = 29;
|
||||
// NEXT ID: 30
|
||||
}
|
||||
|
||||
message NullMessage {
|
||||
|
||||
@ -43,7 +43,7 @@ class TSMessageTest: SSKBaseTest {
|
||||
XCTAssertEqual(now + UInt64(expirationSeconds * 1000), message.expiresAt)
|
||||
}
|
||||
|
||||
func testCanBeRemotelyDeleted() {
|
||||
func canBeRemotelyDeletedByNonAdmin() {
|
||||
let now = Date.ows_millisecondTimestamp()
|
||||
|
||||
do {
|
||||
@ -51,7 +51,7 @@ class TSMessageTest: SSKBaseTest {
|
||||
builder.timestamp = now - UInt64.minuteInMs
|
||||
let message = SSKEnvironment.shared.databaseStorageRef.read { builder.build(transaction: $0) }
|
||||
|
||||
XCTAssert(message.canBeRemotelyDeleted)
|
||||
XCTAssert(message.canBeRemotelyDeletedByNonAdmin)
|
||||
}
|
||||
|
||||
do {
|
||||
@ -59,7 +59,7 @@ class TSMessageTest: SSKBaseTest {
|
||||
builder.timestamp = now - UInt64.minuteInMs
|
||||
let message = builder.build()
|
||||
|
||||
XCTAssertFalse(message.canBeRemotelyDeleted)
|
||||
XCTAssertFalse(message.canBeRemotelyDeletedByNonAdmin)
|
||||
}
|
||||
|
||||
do {
|
||||
@ -71,7 +71,7 @@ class TSMessageTest: SSKBaseTest {
|
||||
message.updateWithRemotelyDeletedAndRemoveRenderableContent(with: transaction)
|
||||
}
|
||||
|
||||
XCTAssertFalse(message.canBeRemotelyDeleted)
|
||||
XCTAssertFalse(message.canBeRemotelyDeletedByNonAdmin)
|
||||
}
|
||||
|
||||
do {
|
||||
@ -80,7 +80,7 @@ class TSMessageTest: SSKBaseTest {
|
||||
builder.giftBadge = OWSGiftBadge(redemptionCredential: Data())
|
||||
let message = SSKEnvironment.shared.databaseStorageRef.read { builder.build(transaction: $0) }
|
||||
|
||||
XCTAssertFalse(message.canBeRemotelyDeleted)
|
||||
XCTAssertFalse(message.canBeRemotelyDeletedByNonAdmin)
|
||||
}
|
||||
|
||||
do {
|
||||
@ -88,7 +88,7 @@ class TSMessageTest: SSKBaseTest {
|
||||
builder.timestamp = now + UInt64.minuteInMs
|
||||
let message = SSKEnvironment.shared.databaseStorageRef.read { builder.build(transaction: $0) }
|
||||
|
||||
XCTAssertTrue(message.canBeRemotelyDeleted)
|
||||
XCTAssertTrue(message.canBeRemotelyDeletedByNonAdmin)
|
||||
}
|
||||
|
||||
do {
|
||||
@ -96,7 +96,7 @@ class TSMessageTest: SSKBaseTest {
|
||||
builder.timestamp = now + (25 * UInt64.hourInMs)
|
||||
let message = SSKEnvironment.shared.databaseStorageRef.read { builder.build(transaction: $0) }
|
||||
|
||||
XCTAssertTrue(message.canBeRemotelyDeleted)
|
||||
XCTAssertTrue(message.canBeRemotelyDeletedByNonAdmin)
|
||||
}
|
||||
|
||||
do {
|
||||
@ -104,7 +104,7 @@ class TSMessageTest: SSKBaseTest {
|
||||
builder.timestamp = now - (25 * UInt64.hourInMs)
|
||||
let message = SSKEnvironment.shared.databaseStorageRef.read { builder.build(transaction: $0) }
|
||||
|
||||
XCTAssertFalse(message.canBeRemotelyDeleted)
|
||||
XCTAssertFalse(message.canBeRemotelyDeletedByNonAdmin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user