Improve story DOE flow

This commit is contained in:
Fedor Indutny 2022-11-28 18:07:26 -08:00 committed by GitHub
parent 5e9744d62a
commit 37d383f344
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 630 additions and 245 deletions

View file

@ -2,12 +2,25 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { noop } from 'lodash';
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
import type { StoryDataType } from '../state/ducks/stories';
import * as Errors from '../types/errors';
import type { StoryMessageRecipientsType } from '../types/Stories';
import * as log from '../logging/log';
import { DAY } from './durations';
import { StoryRecipientUpdateEvent } from '../textsecure/messageReceiverEvents';
import { getSendOptions } from './getSendOptions';
import {
conversationJobQueue,
conversationQueueJobEnum,
} from '../jobs/conversationJobQueue';
import { onStoryRecipientUpdate } from './onStoryRecipientUpdate';
import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage';
import { isGroupV2 } from './whatTypeOfConversation';
import { getMessageById } from '../messages/getMessageById';
import { strictAssert } from './assert';
import { repeat, zipObject } from './iterables';
import { isOlderThan } from './timestamp';
export async function deleteStoryForEveryone(
stories: ReadonlyArray<StoryDataType>,
@ -17,8 +30,31 @@ export async function deleteStoryForEveryone(
return;
}
// Group stories are deleted as regular messages.
const sourceConversation = window.ConversationController.get(
story.conversationId
);
if (sourceConversation && isGroupV2(sourceConversation.attributes)) {
sendDeleteForEveryoneMessage(sourceConversation.attributes, {
deleteForEveryoneDuration: DAY,
id: story.messageId,
timestamp: story.timestamp,
});
return;
}
const logId = `deleteStoryForEveryone(${story.messageId})`;
const message = await getMessageById(story.messageId);
if (!message) {
throw new Error('Story not found');
}
if (isOlderThan(story.timestamp, DAY)) {
throw new Error('Cannot send DOE for a story older than one day');
}
const conversationIds = new Set(Object.keys(story.sendStateByConversationId));
const updatedStoryRecipients = new Map<
const newStoryRecipients = new Map<
string,
{
distributionListIds: Set<string>;
@ -32,6 +68,30 @@ export async function deleteStoryForEveryone(
// Remove ourselves from the DOE.
conversationIds.delete(ourConversation.id);
// `updatedStoryRecipients` is used to build `storyMessageRecipients` for
// a sync message. Put all affected destinationUuids early on so that if
// there are no other distribution lists for them - we'd still include an
// empty list.
Object.entries(story.sendStateByConversationId).forEach(
([recipientId, sendState]) => {
if (recipientId === ourConversation.id) {
return;
}
const destinationUuid =
window.ConversationController.get(recipientId)?.get('uuid');
if (!destinationUuid) {
return;
}
newStoryRecipients.set(destinationUuid, {
distributionListIds: new Set(),
isAllowedToReply: sendState.isAllowedToReplyToStory !== false,
});
}
);
// Find stories that were sent to other distribution lists so that we don't
// send a DOE request to the members of those lists.
stories.forEach(item => {
@ -62,120 +122,95 @@ export async function deleteStoryForEveryone(
return;
}
const distributionListIds =
updatedStoryRecipients.get(destinationUuid)?.distributionListIds ||
new Set();
// These are the remaining distribution list ids that the user has
// access to.
updatedStoryRecipients.set(destinationUuid, {
distributionListIds: item.storyDistributionListId
? new Set([...distributionListIds, item.storyDistributionListId])
: distributionListIds,
isAllowedToReply:
sendStateByConversationId[conversationId].isAllowedToReplyToStory !==
false,
});
// Remove this conversationId so we don't send the DOE to those that
// still have access.
conversationIds.delete(conversationId);
// Build remaining distribution list ids that the user still has
// access to.
if (item.storyDistributionListId === undefined) {
return;
}
// Build complete list of new story recipients (not counting ones that
// are in the deleted story).
let recipient = newStoryRecipients.get(destinationUuid);
if (!recipient) {
const isAllowedToReply =
sendStateByConversationId[conversationId].isAllowedToReplyToStory;
recipient = {
distributionListIds: new Set(),
isAllowedToReply: isAllowedToReply !== false,
};
newStoryRecipients.set(destinationUuid, recipient);
}
recipient.distributionListIds.add(item.storyDistributionListId);
});
});
// Include the sync message with the updated storyMessageRecipients list
const sender = window.textsecure.messaging;
strictAssert(sender, 'messaging has to be initialized');
const newStoryMessageRecipients: StoryMessageRecipientsType = [];
newStoryRecipients.forEach((recipientData, destinationUuid) => {
newStoryMessageRecipients.push({
destinationUuid,
distributionListIds: Array.from(recipientData.distributionListIds),
isAllowedToReply: recipientData.isAllowedToReply,
});
});
const destinationUuid = ourConversation
.getCheckedUuid('deleteStoryForEveryone')
.toString();
log.info(`${logId}: sending DOE to ${conversationIds.size} conversations`);
message.set({
deletedForEveryoneSendStatus: zipObject(conversationIds, repeat(false)),
});
// Send the DOE
conversationIds.forEach(cid => {
// Don't DOE yourself!
if (cid === ourConversation.id) {
return;
}
log.info(`${logId}: enqueing DeleteStoryForEveryone`);
const conversation = window.ConversationController.get(cid);
try {
const jobData: ConversationQueueJobData = {
type: conversationQueueJobEnum.enum.DeleteStoryForEveryone,
conversationId: ourConversation.id,
storyId: story.messageId,
targetTimestamp: story.timestamp,
updatedStoryRecipients: newStoryMessageRecipients,
};
await conversationJobQueue.add(jobData, async jobToInsert => {
log.info(`${logId}: Deleting message with job ${jobToInsert.id}`);
if (!conversation) {
return;
}
sendDeleteForEveryoneMessage(conversation.attributes, {
deleteForEveryoneDuration: DAY,
id: story.messageId,
timestamp: story.timestamp,
});
});
// If it's the last story sent to a distribution list we don't have to send
// the sync message, but to be consistent let's build up the updated
// storyMessageRecipients and send the sync message.
if (!updatedStoryRecipients.size) {
Object.entries(story.sendStateByConversationId).forEach(
([recipientId, sendState]) => {
if (recipientId === ourConversation.id) {
return;
}
const destinationUuid =
window.ConversationController.get(recipientId)?.get('uuid');
if (!destinationUuid) {
return;
}
updatedStoryRecipients.set(destinationUuid, {
distributionListIds: new Set(),
isAllowedToReply: sendState.isAllowedToReplyToStory !== false,
});
}
);
}
// Send the sync message with the updated storyMessageRecipients list
const sender = window.textsecure.messaging;
if (sender) {
const options = await getSendOptions(ourConversation.attributes, {
syncMessage: true,
});
const storyMessageRecipients: Array<{
destinationUuid: string;
distributionListIds: Array<string>;
isAllowedToReply: boolean;
}> = [];
updatedStoryRecipients.forEach((recipientData, destinationUuid) => {
storyMessageRecipients.push({
destinationUuid,
distributionListIds: Array.from(recipientData.distributionListIds),
isAllowedToReply: recipientData.isAllowedToReply,
await window.Signal.Data.saveMessage(message.attributes, {
jobToInsert,
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
});
});
const destinationUuid = ourConversation.get('uuid');
if (!destinationUuid) {
return;
}
// Sync message for other devices
sender.sendSyncMessage({
destination: undefined,
destinationUuid,
storyMessageRecipients,
expirationStartTimestamp: null,
isUpdate: true,
options,
timestamp: story.timestamp,
urgent: false,
});
// Sync message for Desktop
const ev = new StoryRecipientUpdateEvent(
{
destinationUuid,
timestamp: story.timestamp,
storyMessageRecipients,
},
noop
} catch (error) {
log.error(
`${logId}: Failed to queue delete for everyone`,
Errors.toLogFormat(error)
);
onStoryRecipientUpdate(ev);
throw error;
}
log.info(`${logId}: emulating sync message event`);
// Emulate message for Desktop (this will call deleteForEveryone())
const ev = new StoryRecipientUpdateEvent(
{
destinationUuid,
timestamp: story.timestamp,
storyMessageRecipients: newStoryMessageRecipients,
},
noop
);
onStoryRecipientUpdate(ev);
}