Improve story DOE flow
This commit is contained in:
parent
5e9744d62a
commit
37d383f344
15 changed files with 630 additions and 245 deletions
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue