Admin Delete

This commit is contained in:
Jamie
2026-02-27 11:55:02 -08:00
committed by Yash
parent b71b5570d3
commit e424610cc2
67 changed files with 2328 additions and 569 deletions

View File

@@ -0,0 +1,266 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AciString, ServiceIdString } from '../types/ServiceId.std.js';
import type {
ConversationAttributesType,
ReadonlyMessageAttributesType,
} from '../model-types.js';
import { getMessageAge } from './getMessageAge.std.js';
import {
getAdminDeleteMaxAgeMs,
getNormalDeleteMaxAgeMs,
} from './getDeleteMaxAgeMs.dom.js';
import { DAY } from './durations/index.std.js';
import { isGroupV2, isMe } from './whatTypeOfConversation.dom.js';
import { isSignalConversation } from './isSignalConversation.dom.js';
import { getSourceServiceId } from '../messages/sources.preload.js';
import { SignalService as Proto } from '../protobuf/index.std.js';
export type DeleteForEveryoneMessage = Pick<
ReadonlyMessageAttributesType,
| 'type'
| 'sourceServiceId'
| 'sent_at'
| 'serverTimestamp'
| 'deletedForEveryone'
| 'sms'
>;
export type DeleteForEveryoneConversation = Pick<
ConversationAttributesType,
'id' | 'e164' | 'serviceId' | 'groupId' | 'groupVersion'
>;
type Result<T extends object = Record<never, never>> =
| Readonly<{ ok: true } & T>
| Readonly<{ ok: false; reason: string }>;
function checkCommon(
targetConversation: DeleteForEveryoneConversation,
targetMessage: DeleteForEveryoneMessage,
options?: { allowAlreadyDeleted?: boolean }
): Result {
if (isSignalConversation(targetConversation)) {
return { ok: false, reason: 'signal conversation' };
}
if (isMe(targetConversation)) {
return { ok: false, reason: 'note to self conversation' };
}
if (!options?.allowAlreadyDeleted && targetMessage.deletedForEveryone) {
return { ok: false, reason: 'already deleted' };
}
if (targetMessage.sms) {
return { ok: false, reason: 'sms message' };
}
return { ok: true };
}
function canUseNormalDelete(options: {
deleterAci: AciString;
messageAuthorAci: ServiceIdString | undefined;
messageAge: number;
gracePeriodMs?: number;
}): Result {
const {
deleterAci,
messageAuthorAci,
messageAge,
gracePeriodMs = 0,
} = options;
if (deleterAci !== messageAuthorAci) {
return { ok: false, reason: 'not message author' };
}
if (messageAge > getNormalDeleteMaxAgeMs() + gracePeriodMs) {
return { ok: false, reason: 'message is too old' };
}
return { ok: true };
}
function isMemberGroupAdmin(
conversation: Pick<ConversationAttributesType, 'membersV2'>,
aci: AciString
): boolean {
const members = conversation.membersV2 ?? [];
const member = members.find(m => m.aci === aci);
return member?.role === Proto.Member.Role.ADMINISTRATOR;
}
function canUseAdminDelete(options: {
targetConversation: DeleteForEveryoneConversation;
isDeleterGroupAdmin: boolean;
messageAge: number;
gracePeriodMs?: number;
}): Result {
const {
targetConversation,
isDeleterGroupAdmin,
messageAge,
gracePeriodMs = 0,
} = options;
if (!isGroupV2(targetConversation)) {
return { ok: false, reason: 'not a group conversation' };
}
if (!isDeleterGroupAdmin) {
return { ok: false, reason: 'does not have admin role' };
}
if (messageAge > getAdminDeleteMaxAgeMs() + gracePeriodMs) {
return { ok: false, reason: 'message is too old' };
}
return { ok: true };
}
export type CanDeleteForEveryoneOptions = Readonly<{
targetMessage: DeleteForEveryoneMessage;
targetConversation: DeleteForEveryoneConversation;
ourAci: AciString;
isDeleterGroupAdmin: boolean;
}>;
export type CanDeleteForEveryoneResult = Result<{
needsAdminDelete: boolean;
}>;
export function canSendDeleteForEveryone(
options: CanDeleteForEveryoneOptions
): CanDeleteForEveryoneResult {
const { targetConversation, targetMessage, ourAci, isDeleterGroupAdmin } =
options;
const commonCheck = checkCommon(targetConversation, targetMessage);
if (!commonCheck.ok) {
return commonCheck;
}
const messageAuthorAci = getSourceServiceId(targetMessage);
const messageAge = getMessageAge(Date.now(), targetMessage);
// Prefer normal delete for own messages
const normalCheck = canUseNormalDelete({
deleterAci: ourAci,
messageAuthorAci,
messageAge,
});
if (normalCheck.ok) {
return { ok: true, needsAdminDelete: false };
}
// Admin delete for group messages
const adminCheck = canUseAdminDelete({
targetConversation,
isDeleterGroupAdmin,
messageAge,
});
if (adminCheck.ok) {
return { ok: true, needsAdminDelete: true };
}
return { ok: false, reason: 'no permission' };
}
export type CanRetrySendDeleteForEveryoneOptions = Readonly<{
targetMessage: DeleteForEveryoneMessage &
Pick<ReadonlyMessageAttributesType, 'deletedForEveryoneFailed'>;
targetConversation: DeleteForEveryoneConversation;
isAdminDelete: boolean;
isDeleterGroupAdmin: boolean;
ourAci: AciString;
}>;
export type CanRetrySendDeleteForEveryoneResult = Result;
export function canRetrySendDeleteForEveryone(
options: CanRetrySendDeleteForEveryoneOptions
): CanRetrySendDeleteForEveryoneResult {
const {
targetMessage,
targetConversation,
isAdminDelete,
isDeleterGroupAdmin,
ourAci,
} = options;
if (
!targetMessage.deletedForEveryone ||
!targetMessage.deletedForEveryoneFailed
) {
return { ok: false, reason: 'not a failed delete' };
}
const commonCheck = checkCommon(targetConversation, targetMessage, {
allowAlreadyDeleted: true,
});
if (!commonCheck.ok) {
return commonCheck;
}
const messageAuthorAci = getSourceServiceId(targetMessage);
const messageAge = getMessageAge(Date.now(), targetMessage);
if (!isAdminDelete) {
return canUseNormalDelete({
deleterAci: ourAci,
messageAuthorAci,
messageAge,
});
}
return canUseAdminDelete({
targetConversation,
isDeleterGroupAdmin,
messageAge,
});
}
export type CanReceiveDeleteForEveryoneOptions = Readonly<{
targetMessage: DeleteForEveryoneMessage;
targetConversation: ConversationAttributesType;
isAdminDelete: boolean;
deleteServerTimestamp: number;
deleteSentByAci: AciString;
}>;
export type CanReceiveForEveryoneResult = Result;
const MESSAGE_SEND_GRACE_PERIOD = DAY;
export function canReceiveDeleteForEveryone(
options: CanReceiveDeleteForEveryoneOptions
): CanReceiveForEveryoneResult {
const {
targetMessage,
targetConversation,
isAdminDelete,
deleteServerTimestamp,
deleteSentByAci,
} = options;
const commonCheck = checkCommon(targetConversation, targetMessage);
if (!commonCheck.ok) {
return commonCheck;
}
const messageAuthorAci = getSourceServiceId(targetMessage);
const messageAge = getMessageAge(deleteServerTimestamp, targetMessage);
if (!isAdminDelete) {
return canUseNormalDelete({
deleterAci: deleteSentByAci,
messageAuthorAci,
messageAge,
gracePeriodMs: MESSAGE_SEND_GRACE_PERIOD,
});
}
const isDeleterGroupAdmin = isMemberGroupAdmin(
targetConversation,
deleteSentByAci
);
return canUseAdminDelete({
targetConversation,
isDeleterGroupAdmin,
messageAge,
gracePeriodMs: MESSAGE_SEND_GRACE_PERIOD,
});
}

View File

@@ -49,7 +49,7 @@ export async function eraseMessageContents(
| 'view-once-sent'
| 'unsupported-message'
| 'delete-for-everyone',
additionalProperties = {}
additionalProperties: Partial<MessageAttributesType> = {}
): Promise<void> {
log.info(
`Erasing data for message ${getMessageIdForLogging(message.attributes)}: ${reason}`

View File

@@ -2,77 +2,97 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { DeleteAttributesType } from '../messageModifiers/Deletes.preload.js';
import type { MessageAttributesType } from '../model-types.d.ts';
import type { MessageModel } from '../models/messages.preload.js';
import { createLogger } from '../logging/log.std.js';
import { isMe } from './whatTypeOfConversation.dom.js';
import { getAuthorId } from '../messages/sources.preload.js';
import { getSourceServiceId } from '../messages/sources.preload.js';
import { isStory } from '../state/selectors/message.preload.js';
import { isTooOldToModifyMessage } from './isTooOldToModifyMessage.std.js';
import { canReceiveDeleteForEveryone } from './canDeleteForEveryone.preload.js';
import { isAciString } from './isAciString.std.js';
import { eraseMessageContents } from './cleanup.preload.js';
import { notificationService } from '../services/notifications.preload.js';
import { DataWriter } from '../sql/Client.preload.js';
const log = createLogger('deleteForEveryone');
export async function deleteForEveryone(
/**
* Receive path: validate an incoming delete-for-everyone, then apply it.
*/
export async function receiveDeleteForEveryone(
message: MessageModel,
doe: Pick<
DeleteAttributesType,
'fromId' | 'targetSentTimestamp' | 'serverTimestamp'
| 'isAdminDelete'
| 'targetSentTimestamp'
| 'deleteServerTimestamp'
| 'deleteSentByAci'
| 'targetConversationId'
>,
{ shouldPersist = true }: { shouldPersist?: boolean } = {}
): Promise<void> {
if (isDeletionByMe(message, doe)) {
const conversation = window.ConversationController.get(
message.get('conversationId')
);
const conversation = window.ConversationController.get(
message.get('conversationId')
);
// Our 1:1 stories are deleted through ts/util/onStoryRecipientUpdate.ts
if (
isStory(message.attributes) &&
conversation &&
isMe(conversation.attributes)
) {
return;
}
await handleDeleteForEveryone(message, doe, { shouldPersist });
// Our 1:1 stories are deleted through ts/util/onStoryRecipientUpdate.ts
if (
isStory(message.attributes) &&
conversation &&
isMe(conversation.attributes)
) {
return;
}
if (isTooOldToModifyMessage(doe.serverTimestamp, message.attributes)) {
log.warn('Received late DOE. Dropping.', {
fromId: doe.fromId,
const messageAuthorAci = getSourceServiceId(message.attributes);
if (!messageAuthorAci || !isAciString(messageAuthorAci)) {
log.warn('receiveDeleteForEveryone: Cannot determine message author ACI');
return;
}
if (!conversation) {
log.warn('receiveDeleteForEveryone: No conversation found');
return;
}
const result = canReceiveDeleteForEveryone({
isAdminDelete: doe.isAdminDelete,
targetMessage: message.attributes,
targetConversation: conversation.attributes,
deleteSentByAci: doe.deleteSentByAci,
deleteServerTimestamp: doe.deleteServerTimestamp,
});
if (!result.ok) {
log.warn('receiveDeleteForEveryone: Rejected.', {
reason: result.reason,
targetConversationId: doe.targetConversationId,
targetSentTimestamp: doe.targetSentTimestamp,
messageServerTimestamp: message.get('serverTimestamp'),
messageSentAt: message.get('sent_at'),
deleteServerTimestamp: doe.serverTimestamp,
deleteServerTimestamp: doe.deleteServerTimestamp,
});
return;
}
await handleDeleteForEveryone(message, doe, { shouldPersist });
await applyDeleteForEveryone(message, doe, { shouldPersist });
}
function isDeletionByMe(
message: Readonly<MessageModel>,
doe: Pick<DeleteAttributesType, 'fromId'>
): boolean {
const ourConversationId =
window.ConversationController.getOurConversationIdOrThrow();
return (
getAuthorId(message.attributes) === ourConversationId &&
doe.fromId === ourConversationId
);
}
export async function handleDeleteForEveryone(
/**
* Apply a delete-for-everyone to a message. No validation — caller is
* responsible for checking canDeleteForEveryone first.
*/
export async function applyDeleteForEveryone(
message: MessageModel,
del: Pick<
DeleteAttributesType,
'fromId' | 'targetSentTimestamp' | 'serverTimestamp'
| 'isAdminDelete'
| 'targetSentTimestamp'
| 'deleteServerTimestamp'
| 'deleteSentByAci'
| 'targetConversationId'
>,
{ shouldPersist = true }: { shouldPersist?: boolean }
{ shouldPersist = true }: { shouldPersist?: boolean } = {}
): Promise<void> {
if (message.deletingForEveryone || message.get('deletedForEveryone')) {
return;
@@ -80,10 +100,11 @@ export async function handleDeleteForEveryone(
log.info('Handling DOE.', {
messageId: message.id,
fromId: del.fromId,
isAdminDelete: del.isAdminDelete,
targetConversationId: del.targetConversationId,
targetSentTimestamp: del.targetSentTimestamp,
messageServerTimestamp: message.get('serverTimestamp'),
deleteServerTimestamp: del.serverTimestamp,
deleteServerTimestamp: del.deleteServerTimestamp,
});
try {
@@ -94,10 +115,14 @@ export async function handleDeleteForEveryone(
notificationService.removeBy({ messageId: message.get('id') });
// Erase the contents of this message
await eraseMessageContents(message, 'delete-for-everyone', {
const additionalProps: Partial<MessageAttributesType> = {
deletedForEveryone: true,
reactions: [],
});
};
if (del.isAdminDelete) {
additionalProps.deletedForEveryoneByAdminAci = del.deleteSentByAci;
}
await eraseMessageContents(message, 'delete-for-everyone', additionalProps);
if (shouldPersist) {
// We delete the message first, before re-saving it -- this causes any foreign key

View File

@@ -1,7 +1,6 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { DAY } from './durations/index.std.js';
import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage.preload.js';
import { getMessageById } from '../messages/getMessageById.preload.js';
import { createLogger } from '../logging/log.std.js';
@@ -34,7 +33,6 @@ export async function deleteGroupStoryReplyForEveryone(
}
void sendDeleteForEveryoneMessage(group.attributes, {
deleteForEveryoneDuration: DAY,
id: replyMessageId,
timestamp,
});

View File

@@ -41,7 +41,6 @@ export async function deleteStoryForEveryone(
);
if (sourceConversation && isGroupV2(sourceConversation.attributes)) {
void sendDeleteForEveryoneMessage(sourceConversation.attributes, {
deleteForEveryoneDuration: DAY,
id: story.messageId,
timestamp: story.timestamp,
});

View File

@@ -0,0 +1,26 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { getValue } from '../RemoteConfig.dom.js';
import { safeParseInteger } from './numbers.std.js';
import { DAY, SECOND } from './durations/index.std.js';
const DEFAULT_DELETE_MAX_AGE_MS = DAY;
function parseMaxAgeSecsToMs(configValue: string | undefined) {
if (configValue != null) {
const parsed = safeParseInteger(configValue);
if (parsed != null && parsed > 0) {
return parsed * SECOND;
}
}
return DEFAULT_DELETE_MAX_AGE_MS;
}
export function getNormalDeleteMaxAgeMs(): number {
return parseMaxAgeSecsToMs(getValue('global.normalDeleteMaxAgeInSeconds'));
}
export function getAdminDeleteMaxAgeMs(): number {
return parseMaxAgeSecsToMs(getValue('global.adminDeleteMaxAgeInSeconds'));
}

View File

@@ -1,19 +1,71 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AciString } from '../types/ServiceId.std.js';
import type { ConversationAttributesType } from '../model-types.d.ts';
import type { LastMessageType } from '../state/ducks/conversations.preload.js';
import { dropNull } from './dropNull.std.js';
import { findAndFormatContact } from './findAndFormatContact.preload.js';
import { hydrateRanges } from './BodyRange.node.js';
import { stripNewlinesForLeftPane } from './stripNewlinesForLeftPane.std.js';
import { itemStorage } from '../textsecure/Storage.preload.js';
import { getTitle } from './getTitle.preload.js';
function getNameForAci(
aci: AciString | null | undefined,
options?: { isShort?: boolean }
): string | null {
if (aci == null) {
return null;
}
const conversation = window.ConversationController.get(aci);
if (conversation != null) {
return getTitle(conversation.attributes, options);
}
return null;
}
function getDisplayNameForAci(
aci: AciString | null | undefined,
ourAci: AciString | null | undefined
): string | null {
if (aci === ourAci) {
return window.SignalContext.i18n('icu:you');
}
return getNameForAci(aci, { isShort: true });
}
export function getLastMessage(
conversationAttrs: ConversationAttributesType
): LastMessageType | undefined {
const ourAci = itemStorage.user.getAci();
if (conversationAttrs.lastMessageDeletedForEveryone) {
return { deletedForEveryone: true };
const { lastMessageAuthorAci, lastMessageDeletedForEveryoneByAdminAci } =
conversationAttrs;
// Only show admin name when the admin deleted someone else's message
const isAdminDeletingOwnMessage =
lastMessageDeletedForEveryoneByAdminAci != null &&
lastMessageDeletedForEveryoneByAdminAci === lastMessageAuthorAci;
const deletedByAdminName = isAdminDeletingOwnMessage
? null
: getNameForAci(lastMessageDeletedForEveryoneByAdminAci);
const authorName =
getDisplayNameForAci(lastMessageAuthorAci, ourAci) ??
// Deprecated: fall back to lastMessageAuthor from old database rows
conversationAttrs.lastMessageAuthor ??
null;
return {
deletedForEveryone: true,
deletedByAdminName,
isOutgoing: lastMessageAuthorAci === ourAci,
authorName,
};
}
const lastMessageText = conversationAttrs.lastMessage;
if (!lastMessageText) {
return undefined;
@@ -25,8 +77,14 @@ export function getLastMessage(
const text = stripNewlinesForLeftPane(lastMessageText);
const prefix = conversationAttrs.lastMessagePrefix;
const author =
getDisplayNameForAci(conversationAttrs.lastMessageAuthorAci, ourAci) ??
// Deprecated: fall back to lastMessageAuthor from old database rows
conversationAttrs.lastMessageAuthor ??
null;
return {
author: dropNull(conversationAttrs.lastMessageAuthor),
author,
bodyRanges,
deletedForEveryone: false,
prefix,

View File

@@ -0,0 +1,12 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReadonlyMessageAttributesType } from '../model-types.d.ts';
export function getMessageAge(
now: number,
message: Pick<ReadonlyMessageAttributesType, 'serverTimestamp' | 'sent_at'>
): number {
const messageTimestamp = message.serverTimestamp ?? message.sent_at ?? 0;
return Math.abs(now - messageTimestamp);
}

View File

@@ -5,8 +5,11 @@ import type {
ConversationAttributesType,
ReadonlyMessageAttributesType,
} from '../model-types.d.ts';
import type { AciString } from '../types/ServiceId.std.js';
import { isIncoming, isOutgoing } from '../state/selectors/message.preload.js';
import { isAciString } from './isAciString.std.js';
import { getTitle } from './getTitle.preload.js';
import { itemStorage } from '../textsecure/Storage.preload.js';
function getIncomingContact(
messageAttributes: ReadonlyMessageAttributesType
@@ -23,6 +26,7 @@ function getIncomingContact(
.attributes;
}
/** @deprecated Use getMessageAuthorAci instead */
export function getMessageAuthorText(
messageAttributes?: ReadonlyMessageAttributesType
): string | undefined {
@@ -48,3 +52,18 @@ export function getMessageAuthorText(
// it might be a group notification, so we return undefined
return undefined;
}
export function getMessageAuthorAci(
messageAttributes: ReadonlyMessageAttributesType
): AciString | null {
if (isOutgoing(messageAttributes)) {
return itemStorage.user.getCheckedAci();
}
if (isIncoming(messageAttributes)) {
const { sourceServiceId } = messageAttributes;
if (isAciString(sourceServiceId)) {
return sourceServiceId;
}
}
return null;
}

View File

@@ -27,7 +27,7 @@ import { getMessageIdForLogging } from './idForLogging.preload.js';
import { hasErrors } from '../state/selectors/message.preload.js';
import { isIncoming, isOutgoing } from '../messages/helpers.std.js';
import { isDirectConversation } from './whatTypeOfConversation.dom.js';
import { isTooOldToModifyMessage } from './isTooOldToModifyMessage.std.js';
import { isTooOldToEditMessage } from './isTooOldToEditMessage.std.js';
import { queueAttachmentDownloads } from './queueAttachmentDownloads.preload.js';
import { modifyTargetMessage } from './modifyTargetMessage.preload.js';
import { isMessageNoteToSelf } from './isMessageNoteToSelf.dom.js';
@@ -93,7 +93,7 @@ export async function handleEditMessage(
if (
serverTimestamp &&
!isMessageNoteToSelf(mainMessage) &&
isTooOldToModifyMessage(serverTimestamp, mainMessage)
isTooOldToEditMessage(serverTimestamp, mainMessage)
) {
log.warn(`${idLog}: cannot edit message older than 48h`, serverTimestamp);
return;

View File

@@ -0,0 +1,18 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isFeaturedEnabledNoRedux } from './isFeatureEnabled.dom.js';
export function isAdminDeleteReceiveEnabled(): boolean {
return isFeaturedEnabledNoRedux({
betaKey: 'desktop.adminDelete.receive.beta',
prodKey: 'desktop.adminDelete.receive.prod',
});
}
export function isAdminDeleteSendEnabled(): boolean {
return isFeaturedEnabledNoRedux({
betaKey: 'desktop.adminDelete.send.beta',
prodKey: 'desktop.adminDelete.send.prod',
});
}

View File

@@ -1,14 +1,13 @@
// Copyright 2023 Signal Messenger, LLC
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReadonlyMessageAttributesType } from '../model-types.d.ts';
import { getMessageAge } from './getMessageAge.std.js';
import { DAY } from './durations/index.std.js';
export function isTooOldToModifyMessage(
export function isTooOldToEditMessage(
serverTimestamp: number,
message: Pick<ReadonlyMessageAttributesType, 'serverTimestamp' | 'sent_at'>
): boolean {
const messageTimestamp = message.serverTimestamp || message.sent_at || 0;
const delta = Math.abs(serverTimestamp - messageTimestamp);
return delta > DAY * 2;
return getMessageAge(serverTimestamp, message) > DAY * 2;
}

View File

@@ -23,7 +23,7 @@ import {
sendStateReducer,
} from '../messages/MessageSendState.std.js';
import { canConversationBeUnarchived } from './canConversationBeUnarchived.preload.js';
import { deleteForEveryone } from './deleteForEveryone.preload.js';
import { receiveDeleteForEveryone } from './deleteForEveryone.preload.js';
import { drop } from './drop.std.js';
import { handleEditMessage } from './handleEditMessage.preload.js';
import { isGroup } from './whatTypeOfConversation.dom.js';
@@ -349,7 +349,9 @@ export async function modifyTargetMessage(
const deletes = Deletes.forMessage(message.attributes);
await Promise.all(
deletes.map(async del => {
await deleteForEveryone(message, del, { shouldPersist: !isFirstRun });
await receiveDeleteForEveryone(message, del, {
shouldPersist: !isFirstRun,
});
changed = true;
})
);

View File

@@ -13,7 +13,8 @@ import { queueUpdateMessage } from './messageBatcher.preload.js';
import { isMe } from './whatTypeOfConversation.dom.js';
import { drop } from './drop.std.js';
import { fromServiceIdBinaryOrString } from './ServiceId.node.js';
import { handleDeleteForEveryone } from './deleteForEveryone.preload.js';
import { applyDeleteForEveryone } from './deleteForEveryone.preload.js';
import { itemStorage } from '../textsecure/Storage.preload.js';
import { MessageModel } from '../models/messages.preload.js';
const { isEqual } = lodash;
@@ -199,12 +200,14 @@ export async function onStoryRecipientUpdate(
// sent timestamp doesn't happen (it would return all copies of the
// story, not just the one we want to delete).
drop(
handleDeleteForEveryone(
applyDeleteForEveryone(
message,
{
fromId: ourConversationId,
serverTimestamp: Number(item.serverTimestamp),
isAdminDelete: false,
targetSentTimestamp: item.timestamp,
deleteServerTimestamp: Number(item.serverTimestamp),
deleteSentByAci: itemStorage.user.getCheckedAci(),
targetConversationId: ourConversationId,
},
{ shouldPersist: true }
)

View File

@@ -4,13 +4,12 @@
import type { ConversationAttributesType } from '../model-types.d.ts';
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue.preload.js';
import * as Errors from '../types/errors.std.js';
import { DAY } from './durations/index.std.js';
import { createLogger } from '../logging/log.std.js';
import {
conversationJobQueue,
conversationQueueJobEnum,
} from '../jobs/conversationJobQueue.preload.js';
import { deleteForEveryone } from './deleteForEveryone.preload.js';
import { applyDeleteForEveryone } from './deleteForEveryone.preload.js';
import {
getConversationIdForLogging,
getMessageIdForLogging,
@@ -19,40 +18,53 @@ import { getMessageById } from '../messages/getMessageById.preload.js';
import { getRecipientConversationIds } from './getRecipientConversationIds.dom.js';
import { getRecipients } from './getRecipients.dom.js';
import { repeat, zipObject } from './iterables.std.js';
import { isMe } from './whatTypeOfConversation.dom.js';
import { isOutgoing } from '../state/selectors/message.preload.js';
import { canSendDeleteForEveryone } from './canDeleteForEveryone.preload.js';
import { areWeAdmin } from './areWeAdmin.preload.js';
import { isAciString } from './isAciString.std.js';
import { itemStorage } from '../textsecure/Storage.preload.js';
import { strictAssert } from './assert.std.js';
const log = createLogger('sendDeleteForEveryoneMessage');
export async function sendDeleteForEveryoneMessage(
conversationAttributes: ConversationAttributesType,
options: {
deleteForEveryoneDuration?: number;
id: string;
timestamp: number;
}
): Promise<void> {
const {
deleteForEveryoneDuration,
timestamp: targetTimestamp,
id: messageId,
} = options;
const { timestamp: targetTimestamp, id: messageId } = options;
const message = await getMessageById(messageId);
if (!message) {
throw new Error('sendDeleteForEveryoneMessage: Cannot find message!');
}
const idForLogging = getMessageIdForLogging(message.attributes);
// If conversation is a Note To Self, no deletion time limits apply.
if (!isMe(conversationAttributes)) {
const timestamp = Date.now();
const maxDuration = deleteForEveryoneDuration || DAY;
if (timestamp - targetTimestamp > maxDuration) {
throw new Error(
`Cannot send DOE for a message older than ${maxDuration}`
);
}
const ourAci = itemStorage.user.getCheckedAci();
const { sourceServiceId } = message.attributes;
const messageAuthorAci = isOutgoing(message.attributes)
? ourAci
: sourceServiceId;
strictAssert(
isAciString(messageAuthorAci),
'sendDeleteForEveryoneMessage: Needs message author ACI'
);
const result = canSendDeleteForEveryone({
targetMessage: message.attributes,
targetConversation: conversationAttributes,
ourAci,
isDeleterGroupAdmin: areWeAdmin(conversationAttributes),
});
if (!result.ok) {
throw new Error(`Cannot send DOE: ${result.reason}`);
}
const { needsAdminDelete: isAdminDelete } = result;
message.set({
deletedForEveryoneSendStatus: zipObject(
getRecipientConversationIds(conversationAttributes),
@@ -66,17 +78,17 @@ export async function sendDeleteForEveryoneMessage(
log.info(
`enqueuing DeleteForEveryone: ${idForLogging} ` +
`in conversation ${conversationIdForLogging}`
`in conversation ${conversationIdForLogging} (isAdminDelete=${isAdminDelete})`
);
try {
const jobData: ConversationQueueJobData = {
type: conversationQueueJobEnum.enum.DeleteForEveryone,
conversationId: conversationAttributes.id,
messageId,
isAdminDelete,
targetMessageId: messageId,
recipients: getRecipients(conversationAttributes),
revision: conversationAttributes.revision,
targetTimestamp,
};
await conversationJobQueue.add(jobData, async jobToInsert => {
log.info(
@@ -95,9 +107,12 @@ export async function sendDeleteForEveryoneMessage(
throw error;
}
await deleteForEveryone(message, {
await applyDeleteForEveryone(message, {
isAdminDelete,
targetSentTimestamp: targetTimestamp,
serverTimestamp: Date.now(),
fromId: window.ConversationController.getOurConversationIdOrThrow(),
deleteServerTimestamp: Date.now(),
deleteSentByAci: ourAci,
targetConversationId:
window.ConversationController.getOurConversationIdOrThrow(),
});
}