DOE for stories
This commit is contained in:
parent
d7307934bc
commit
5639c1adea
8 changed files with 381 additions and 15 deletions
|
@ -154,6 +154,7 @@ import { conversationJobQueue } from './jobs/conversationJobQueue';
|
|||
import { SeenStatus } from './MessageSeenStatus';
|
||||
import MessageSender from './textsecure/SendMessage';
|
||||
import type AccountManager from './textsecure/AccountManager';
|
||||
import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate';
|
||||
import { validateConversation } from './util/validateConversation';
|
||||
|
||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||
|
@ -398,6 +399,10 @@ export async function startApp(): Promise<void> {
|
|||
'pniIdentity',
|
||||
queuedEventListener(onPNIIdentitySync)
|
||||
);
|
||||
messageReceiver.addEventListener(
|
||||
'storyRecipientUpdate',
|
||||
queuedEventListener(onStoryRecipientUpdate, false)
|
||||
);
|
||||
});
|
||||
|
||||
ourProfileKeyService.initialize(window.storage);
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Collection, Model } from 'backbone';
|
|||
import type { MessageModel } from '../models/messages';
|
||||
import { getContactId } from '../messages/helpers';
|
||||
import * as log from '../logging/log';
|
||||
import { deleteForEveryone } from '../util/deleteForEveryone';
|
||||
|
||||
export type DeleteAttributesType = {
|
||||
targetSentTimestamp: number;
|
||||
|
@ -73,7 +74,7 @@ export class Deletes extends Collection<DeleteModel> {
|
|||
);
|
||||
|
||||
const targetMessage = messages.find(
|
||||
m => del.get('fromId') === getContactId(m)
|
||||
m => del.get('fromId') === getContactId(m) && !m.deletedForEveryone
|
||||
);
|
||||
|
||||
if (!targetMessage) {
|
||||
|
@ -91,7 +92,7 @@ export class Deletes extends Collection<DeleteModel> {
|
|||
targetMessage
|
||||
);
|
||||
|
||||
await window.Signal.Util.deleteForEveryone(message, del);
|
||||
await deleteForEveryone(message, del);
|
||||
|
||||
this.remove(del);
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
repeat,
|
||||
zipObject,
|
||||
} from '../util/iterables';
|
||||
import type { DeleteModel } from '../messageModifiers/Deletes';
|
||||
import type { SentEventData } from '../textsecure/messageReceiverEvents';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import { isNormalNumber } from '../util/isNormalNumber';
|
||||
|
@ -160,7 +161,6 @@ import { isNewReactionReplacingPrevious } from '../reactions/util';
|
|||
import { parseBoostBadgeListFromServer } from '../badges/parseBadgesFromServer';
|
||||
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||
import { downloadAttachment } from '../util/downloadAttachment';
|
||||
import type { DeleteModel } from '../messageModifiers/Deletes';
|
||||
import type { StickerWithHydratedData } from '../types/Stickers';
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
|
||||
import { isEqual, pick } from 'lodash';
|
||||
import { isEqual, noop, pick } from 'lodash';
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import type { BodyRangeType } from '../../types/Util';
|
||||
import type { MessageAttributesType } from '../../model-types.d';
|
||||
|
@ -19,6 +19,7 @@ import dataInterface from '../../sql/Client';
|
|||
import { DAY } from '../../util/durations';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
|
||||
import { StoryRecipientUpdateEvent } from '../../textsecure/messageReceiverEvents';
|
||||
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
|
||||
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
|
||||
import { getMessageById } from '../../messages/getMessageById';
|
||||
|
@ -33,8 +34,10 @@ import {
|
|||
isDownloading,
|
||||
} from '../../types/Attachment';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import { getSendOptions } from '../../util/getSendOptions';
|
||||
import { getStories } from '../selectors/stories';
|
||||
import { isGroup } from '../../util/whatTypeOfConversation';
|
||||
import { onStoryRecipientUpdate } from '../../util/onStoryRecipientUpdate';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
||||
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
||||
|
@ -154,7 +157,7 @@ export type StoriesActionType =
|
|||
function deleteStoryForEveryone(
|
||||
story: StoryViewType
|
||||
): ThunkAction<void, RootStateType, unknown, DOEStoryActionType> {
|
||||
return (dispatch, getState) => {
|
||||
return async (dispatch, getState) => {
|
||||
if (!story.sendState) {
|
||||
return;
|
||||
}
|
||||
|
@ -162,25 +165,79 @@ function deleteStoryForEveryone(
|
|||
const conversationIds = new Set(
|
||||
story.sendState.map(({ recipient }) => recipient.id)
|
||||
);
|
||||
const updatedStoryRecipients = new Map<
|
||||
string,
|
||||
{
|
||||
distributionListIds: Set<string>;
|
||||
isAllowedToReply: boolean;
|
||||
}
|
||||
>();
|
||||
|
||||
const ourConversation =
|
||||
window.ConversationController.getOurConversationOrThrow();
|
||||
|
||||
// Remove ourselves from the DOE.
|
||||
conversationIds.delete(ourConversation.id);
|
||||
|
||||
// Find stories that were sent to other distribution lists so that we don't
|
||||
// send a DOE request to the members of those lists.
|
||||
const { stories } = getState().stories;
|
||||
stories.forEach(item => {
|
||||
if (item.timestamp !== story.timestamp) {
|
||||
const { sendStateByConversationId } = item;
|
||||
// We only want matching timestamp stories which are stories that were
|
||||
// sent to multi distribution lists.
|
||||
// We don't want the story we just passed in.
|
||||
// Don't need to check for stories that have already been deleted.
|
||||
// And only for sent stories, not incoming.
|
||||
if (
|
||||
item.timestamp !== story.timestamp ||
|
||||
item.messageId === story.messageId ||
|
||||
item.deletedForEveryone ||
|
||||
!sendStateByConversationId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.sendStateByConversationId) {
|
||||
return;
|
||||
}
|
||||
Object.keys(sendStateByConversationId).forEach(conversationId => {
|
||||
if (conversationId === ourConversation.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(item.sendStateByConversationId).forEach(conversationId => {
|
||||
const destinationUuid =
|
||||
window.ConversationController.get(conversationId)?.get('uuid');
|
||||
|
||||
if (!destinationUuid) {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// Send the DOE
|
||||
conversationIds.forEach(cid => {
|
||||
// Don't DOE yourself!
|
||||
if (cid === ourConversation.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conversation = window.ConversationController.get(cid);
|
||||
|
||||
if (!conversation) {
|
||||
|
@ -194,6 +251,81 @@ function deleteStoryForEveryone(
|
|||
});
|
||||
});
|
||||
|
||||
// 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) {
|
||||
story.sendState.forEach(item => {
|
||||
if (item.recipient.id === ourConversation.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const destinationUuid = window.ConversationController.get(
|
||||
item.recipient.id
|
||||
)?.get('uuid');
|
||||
|
||||
if (!destinationUuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
updatedStoryRecipients.set(destinationUuid, {
|
||||
distributionListIds: new Set(),
|
||||
isAllowedToReply: item.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,
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
onStoryRecipientUpdate(ev);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: DOE_STORY,
|
||||
payload: story.messageId,
|
||||
|
|
|
@ -109,6 +109,7 @@ import {
|
|||
ContactSyncEvent,
|
||||
GroupEvent,
|
||||
GroupSyncEvent,
|
||||
StoryRecipientUpdateEvent,
|
||||
} from './messageReceiverEvents';
|
||||
import * as log from '../logging/log';
|
||||
import * as durations from '../util/durations';
|
||||
|
@ -579,6 +580,11 @@ export default class MessageReceiver
|
|||
handler: (ev: EnvelopeEvent) => void
|
||||
): void;
|
||||
|
||||
public override addEventListener(
|
||||
name: 'storyRecipientUpdate',
|
||||
handler: (ev: StoryRecipientUpdateEvent) => void
|
||||
): void;
|
||||
|
||||
public override addEventListener(name: string, handler: EventHandler): void {
|
||||
return super.addEventListener(name, handler);
|
||||
}
|
||||
|
@ -1821,7 +1827,8 @@ export default class MessageReceiver
|
|||
const ev = new SentEvent(
|
||||
{
|
||||
destination: dropNull(destination),
|
||||
destinationUuid: dropNull(destinationUuid),
|
||||
destinationUuid:
|
||||
dropNull(destinationUuid) || envelope.destinationUuid.toString(),
|
||||
timestamp: timestamp?.toNumber(),
|
||||
serverTimestamp: envelope.serverTimestamp,
|
||||
device: envelope.sourceDevice,
|
||||
|
@ -1931,7 +1938,7 @@ export default class MessageReceiver
|
|||
|
||||
isAllowedToReply.set(
|
||||
destinationUuid,
|
||||
Boolean(recipient.isAllowedToReply)
|
||||
recipient.isAllowedToReply !== false
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -2572,6 +2579,18 @@ export default class MessageReceiver
|
|||
return;
|
||||
}
|
||||
|
||||
if (sentMessage.storyMessageRecipients && sentMessage.isRecipientUpdate) {
|
||||
const ev = new StoryRecipientUpdateEvent(
|
||||
{
|
||||
destinationUuid: envelope.destinationUuid.toString(),
|
||||
timestamp: envelope.timestamp,
|
||||
storyMessageRecipients: sentMessage.storyMessageRecipients,
|
||||
},
|
||||
this.removeFromCache.bind(this, envelope)
|
||||
);
|
||||
return this.dispatchAndWait(ev);
|
||||
}
|
||||
|
||||
if (!sentMessage || !sentMessage.message) {
|
||||
throw new Error(
|
||||
'MessageReceiver.handleSyncMessage: sync sent message was missing message'
|
||||
|
|
|
@ -1232,8 +1232,9 @@ export default class MessageSender {
|
|||
isUpdate,
|
||||
urgent,
|
||||
options,
|
||||
storyMessageRecipients,
|
||||
}: Readonly<{
|
||||
encodedDataMessage: Uint8Array;
|
||||
encodedDataMessage?: Uint8Array;
|
||||
timestamp: number;
|
||||
destination: string | undefined;
|
||||
destinationUuid: string | null | undefined;
|
||||
|
@ -1243,13 +1244,21 @@ export default class MessageSender {
|
|||
isUpdate?: boolean;
|
||||
urgent: boolean;
|
||||
options?: SendOptionsType;
|
||||
storyMessageRecipients?: Array<{
|
||||
destinationUuid: string;
|
||||
distributionListIds: Array<string>;
|
||||
isAllowedToReply: boolean;
|
||||
}>;
|
||||
}>): Promise<CallbackResultType> {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const dataMessage = Proto.DataMessage.decode(encodedDataMessage);
|
||||
const sentMessage = new Proto.SyncMessage.Sent();
|
||||
sentMessage.timestamp = Long.fromNumber(timestamp);
|
||||
sentMessage.message = dataMessage;
|
||||
|
||||
if (encodedDataMessage) {
|
||||
const dataMessage = Proto.DataMessage.decode(encodedDataMessage);
|
||||
sentMessage.message = dataMessage;
|
||||
}
|
||||
if (destination) {
|
||||
sentMessage.destination = destination;
|
||||
}
|
||||
|
@ -1261,6 +1270,19 @@ export default class MessageSender {
|
|||
expirationStartTimestamp
|
||||
);
|
||||
}
|
||||
if (storyMessageRecipients) {
|
||||
sentMessage.storyMessageRecipients = storyMessageRecipients.map(
|
||||
recipient => {
|
||||
const storyMessageRecipient =
|
||||
new Proto.SyncMessage.Sent.StoryMessageRecipient();
|
||||
storyMessageRecipient.destinationUuid = recipient.destinationUuid;
|
||||
storyMessageRecipient.distributionListIds =
|
||||
recipient.distributionListIds;
|
||||
storyMessageRecipient.isAllowedToReply = recipient.isAllowedToReply;
|
||||
return storyMessageRecipient;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (isUpdate) {
|
||||
sentMessage.isRecipientUpdate = true;
|
||||
|
|
|
@ -418,3 +418,18 @@ export class ViewSyncEvent extends ConfirmableEvent {
|
|||
super('viewSync', confirm);
|
||||
}
|
||||
}
|
||||
|
||||
export type StoryRecipientUpdateData = Readonly<{
|
||||
destinationUuid: string;
|
||||
storyMessageRecipients: Array<Proto.SyncMessage.Sent.IStoryMessageRecipient>;
|
||||
timestamp: number;
|
||||
}>;
|
||||
|
||||
export class StoryRecipientUpdateEvent extends ConfirmableEvent {
|
||||
constructor(
|
||||
public readonly data: StoryRecipientUpdateData,
|
||||
confirm: ConfirmCallback
|
||||
) {
|
||||
super('storyRecipientUpdate', confirm);
|
||||
}
|
||||
}
|
||||
|
|
172
ts/util/onStoryRecipientUpdate.ts
Normal file
172
ts/util/onStoryRecipientUpdate.ts
Normal file
|
@ -0,0 +1,172 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import type { DeleteAttributesType } from '../messageModifiers/Deletes';
|
||||
import type { StoryRecipientUpdateEvent } from '../textsecure/messageReceiverEvents';
|
||||
import * as log from '../logging/log';
|
||||
import { Deletes } from '../messageModifiers/Deletes';
|
||||
import { SendStatus } from '../messages/MessageSendState';
|
||||
import { deleteForEveryone } from './deleteForEveryone';
|
||||
import {
|
||||
getConversationIdForLogging,
|
||||
getMessageIdForLogging,
|
||||
} from './idForLogging';
|
||||
import { isStory } from '../state/selectors/message';
|
||||
import { queueUpdateMessage } from './messageBatcher';
|
||||
|
||||
export async function onStoryRecipientUpdate(
|
||||
event: StoryRecipientUpdateEvent
|
||||
): Promise<void> {
|
||||
const { data, confirm } = event;
|
||||
|
||||
const { destinationUuid, timestamp } = data;
|
||||
|
||||
const conversation = window.ConversationController.get(destinationUuid);
|
||||
|
||||
if (!conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetConversation =
|
||||
await window.ConversationController.getConversationForTargetMessage(
|
||||
conversation.id,
|
||||
timestamp
|
||||
);
|
||||
|
||||
if (!targetConversation) {
|
||||
log.info('onStoryRecipientUpdate !targetConversation', {
|
||||
destinationUuid,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
targetConversation.queueJob('onStoryRecipientUpdate', async () => {
|
||||
log.info('onStoryRecipientUpdate updating', timestamp);
|
||||
|
||||
// Build up some maps for fast/easy lookups
|
||||
const isAllowedToReply = new Map<string, boolean>();
|
||||
const conversationIdToDistributionListIds = new Map<string, Set<string>>();
|
||||
data.storyMessageRecipients.forEach(item => {
|
||||
const convo = window.ConversationController.get(item.destinationUuid);
|
||||
|
||||
if (!convo || !item.distributionListIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
conversationIdToDistributionListIds.set(
|
||||
convo.id,
|
||||
new Set(item.distributionListIds)
|
||||
);
|
||||
isAllowedToReply.set(convo.id, item.isAllowedToReply !== false);
|
||||
});
|
||||
|
||||
const ourConversationId =
|
||||
window.ConversationController.getOurConversationIdOrThrow();
|
||||
const now = Date.now();
|
||||
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(timestamp);
|
||||
|
||||
// Now we figure out who needs to be added and who needs to removed
|
||||
messages.forEach(item => {
|
||||
if (!isStory(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sendStateByConversationId, storyDistributionListId } = item;
|
||||
|
||||
if (!sendStateByConversationId || !storyDistributionListId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSendStateByConversationId = {
|
||||
...sendStateByConversationId,
|
||||
};
|
||||
|
||||
conversationIdToDistributionListIds.forEach(
|
||||
(distributionListIds, conversationId) => {
|
||||
const hasDistributionListId = distributionListIds.has(
|
||||
storyDistributionListId
|
||||
);
|
||||
|
||||
const recipient = window.ConversationController.get(conversationId);
|
||||
const conversationIdForLogging = recipient
|
||||
? getConversationIdForLogging(recipient.attributes)
|
||||
: conversationId;
|
||||
|
||||
if (
|
||||
hasDistributionListId &&
|
||||
!sendStateByConversationId[conversationId]
|
||||
) {
|
||||
log.info('onStoryRecipientUpdate adding', {
|
||||
conversationId: conversationIdForLogging,
|
||||
messageId: getMessageIdForLogging(item),
|
||||
storyDistributionListId,
|
||||
});
|
||||
nextSendStateByConversationId[conversationId] = {
|
||||
isAllowedToReplyToStory: Boolean(
|
||||
isAllowedToReply.get(conversationId)
|
||||
),
|
||||
status: SendStatus.Sent,
|
||||
updatedAt: now,
|
||||
};
|
||||
} else if (
|
||||
sendStateByConversationId[conversationId] &&
|
||||
!hasDistributionListId
|
||||
) {
|
||||
log.info('onStoryRecipientUpdate removing', {
|
||||
conversationId: conversationIdForLogging,
|
||||
messageId: getMessageIdForLogging(item),
|
||||
storyDistributionListId,
|
||||
});
|
||||
delete nextSendStateByConversationId[conversationId];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (isEqual(sendStateByConversationId, nextSendStateByConversationId)) {
|
||||
log.info(
|
||||
'onStoryRecipientUpdate: sendStateByConversationId does not need update'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = window.MessageController.register(item.id, item);
|
||||
|
||||
const sendStateConversationIds = new Set(
|
||||
Object.keys(nextSendStateByConversationId)
|
||||
);
|
||||
|
||||
if (
|
||||
sendStateConversationIds.size === 0 ||
|
||||
(sendStateConversationIds.size === 1 &&
|
||||
sendStateConversationIds.has(ourConversationId))
|
||||
) {
|
||||
log.info('onStoryRecipientUpdate DOE', {
|
||||
messageId: getMessageIdForLogging(item),
|
||||
storyDistributionListId,
|
||||
});
|
||||
const delAttributes: DeleteAttributesType = {
|
||||
fromId: ourConversationId,
|
||||
serverTimestamp: Number(item.serverTimestamp),
|
||||
targetSentTimestamp: item.timestamp,
|
||||
};
|
||||
const doe = Deletes.getSingleton().add(delAttributes);
|
||||
// There are no longer any remaining members for this message so lets
|
||||
// run it through deleteForEveryone which marks the message as
|
||||
// deletedForEveryone locally.
|
||||
deleteForEveryone(message, doe);
|
||||
} else {
|
||||
message.set({
|
||||
sendStateByConversationId: nextSendStateByConversationId,
|
||||
});
|
||||
queueUpdateMessage(message.attributes);
|
||||
}
|
||||
});
|
||||
|
||||
window.Whisper.events.trigger('incrementProgress');
|
||||
confirm();
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue