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 { SeenStatus } from './MessageSeenStatus';
|
||||||
import MessageSender from './textsecure/SendMessage';
|
import MessageSender from './textsecure/SendMessage';
|
||||||
import type AccountManager from './textsecure/AccountManager';
|
import type AccountManager from './textsecure/AccountManager';
|
||||||
|
import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate';
|
||||||
import { validateConversation } from './util/validateConversation';
|
import { validateConversation } from './util/validateConversation';
|
||||||
|
|
||||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||||
|
@ -398,6 +399,10 @@ export async function startApp(): Promise<void> {
|
||||||
'pniIdentity',
|
'pniIdentity',
|
||||||
queuedEventListener(onPNIIdentitySync)
|
queuedEventListener(onPNIIdentitySync)
|
||||||
);
|
);
|
||||||
|
messageReceiver.addEventListener(
|
||||||
|
'storyRecipientUpdate',
|
||||||
|
queuedEventListener(onStoryRecipientUpdate, false)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ourProfileKeyService.initialize(window.storage);
|
ourProfileKeyService.initialize(window.storage);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Collection, Model } from 'backbone';
|
||||||
import type { MessageModel } from '../models/messages';
|
import type { MessageModel } from '../models/messages';
|
||||||
import { getContactId } from '../messages/helpers';
|
import { getContactId } from '../messages/helpers';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
import { deleteForEveryone } from '../util/deleteForEveryone';
|
||||||
|
|
||||||
export type DeleteAttributesType = {
|
export type DeleteAttributesType = {
|
||||||
targetSentTimestamp: number;
|
targetSentTimestamp: number;
|
||||||
|
@ -73,7 +74,7 @@ export class Deletes extends Collection<DeleteModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
const targetMessage = messages.find(
|
const targetMessage = messages.find(
|
||||||
m => del.get('fromId') === getContactId(m)
|
m => del.get('fromId') === getContactId(m) && !m.deletedForEveryone
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!targetMessage) {
|
if (!targetMessage) {
|
||||||
|
@ -91,7 +92,7 @@ export class Deletes extends Collection<DeleteModel> {
|
||||||
targetMessage
|
targetMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
await window.Signal.Util.deleteForEveryone(message, del);
|
await deleteForEveryone(message, del);
|
||||||
|
|
||||||
this.remove(del);
|
this.remove(del);
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
repeat,
|
repeat,
|
||||||
zipObject,
|
zipObject,
|
||||||
} from '../util/iterables';
|
} from '../util/iterables';
|
||||||
|
import type { DeleteModel } from '../messageModifiers/Deletes';
|
||||||
import type { SentEventData } from '../textsecure/messageReceiverEvents';
|
import type { SentEventData } from '../textsecure/messageReceiverEvents';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
import { isNotNil } from '../util/isNotNil';
|
||||||
import { isNormalNumber } from '../util/isNormalNumber';
|
import { isNormalNumber } from '../util/isNormalNumber';
|
||||||
|
@ -160,7 +161,6 @@ import { isNewReactionReplacingPrevious } from '../reactions/util';
|
||||||
import { parseBoostBadgeListFromServer } from '../badges/parseBadgesFromServer';
|
import { parseBoostBadgeListFromServer } from '../badges/parseBadgesFromServer';
|
||||||
import { GiftBadgeStates } from '../components/conversation/Message';
|
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||||
import { downloadAttachment } from '../util/downloadAttachment';
|
import { downloadAttachment } from '../util/downloadAttachment';
|
||||||
import type { DeleteModel } from '../messageModifiers/Deletes';
|
|
||||||
import type { StickerWithHydratedData } from '../types/Stickers';
|
import type { StickerWithHydratedData } from '../types/Stickers';
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
|
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 { AttachmentType } from '../../types/Attachment';
|
||||||
import type { BodyRangeType } from '../../types/Util';
|
import type { BodyRangeType } from '../../types/Util';
|
||||||
import type { MessageAttributesType } from '../../model-types.d';
|
import type { MessageAttributesType } from '../../model-types.d';
|
||||||
|
@ -19,6 +19,7 @@ import dataInterface from '../../sql/Client';
|
||||||
import { DAY } from '../../util/durations';
|
import { DAY } from '../../util/durations';
|
||||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
|
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
|
||||||
|
import { StoryRecipientUpdateEvent } from '../../textsecure/messageReceiverEvents';
|
||||||
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
|
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
|
||||||
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
|
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
|
||||||
import { getMessageById } from '../../messages/getMessageById';
|
import { getMessageById } from '../../messages/getMessageById';
|
||||||
|
@ -33,8 +34,10 @@ import {
|
||||||
isDownloading,
|
isDownloading,
|
||||||
} from '../../types/Attachment';
|
} from '../../types/Attachment';
|
||||||
import { getConversationSelector } from '../selectors/conversations';
|
import { getConversationSelector } from '../selectors/conversations';
|
||||||
|
import { getSendOptions } from '../../util/getSendOptions';
|
||||||
import { getStories } from '../selectors/stories';
|
import { getStories } from '../selectors/stories';
|
||||||
import { isGroup } from '../../util/whatTypeOfConversation';
|
import { isGroup } from '../../util/whatTypeOfConversation';
|
||||||
|
import { onStoryRecipientUpdate } from '../../util/onStoryRecipientUpdate';
|
||||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
||||||
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
||||||
|
@ -154,7 +157,7 @@ export type StoriesActionType =
|
||||||
function deleteStoryForEveryone(
|
function deleteStoryForEveryone(
|
||||||
story: StoryViewType
|
story: StoryViewType
|
||||||
): ThunkAction<void, RootStateType, unknown, DOEStoryActionType> {
|
): ThunkAction<void, RootStateType, unknown, DOEStoryActionType> {
|
||||||
return (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
if (!story.sendState) {
|
if (!story.sendState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -162,25 +165,79 @@ function deleteStoryForEveryone(
|
||||||
const conversationIds = new Set(
|
const conversationIds = new Set(
|
||||||
story.sendState.map(({ recipient }) => recipient.id)
|
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
|
// Find stories that were sent to other distribution lists so that we don't
|
||||||
// send a DOE request to the members of those lists.
|
// send a DOE request to the members of those lists.
|
||||||
const { stories } = getState().stories;
|
const { stories } = getState().stories;
|
||||||
stories.forEach(item => {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!item.sendStateByConversationId) {
|
Object.keys(sendStateByConversationId).forEach(conversationId => {
|
||||||
return;
|
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);
|
conversationIds.delete(conversationId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Send the DOE
|
||||||
conversationIds.forEach(cid => {
|
conversationIds.forEach(cid => {
|
||||||
|
// Don't DOE yourself!
|
||||||
|
if (cid === ourConversation.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const conversation = window.ConversationController.get(cid);
|
const conversation = window.ConversationController.get(cid);
|
||||||
|
|
||||||
if (!conversation) {
|
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({
|
dispatch({
|
||||||
type: DOE_STORY,
|
type: DOE_STORY,
|
||||||
payload: story.messageId,
|
payload: story.messageId,
|
||||||
|
|
|
@ -109,6 +109,7 @@ import {
|
||||||
ContactSyncEvent,
|
ContactSyncEvent,
|
||||||
GroupEvent,
|
GroupEvent,
|
||||||
GroupSyncEvent,
|
GroupSyncEvent,
|
||||||
|
StoryRecipientUpdateEvent,
|
||||||
} from './messageReceiverEvents';
|
} from './messageReceiverEvents';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import * as durations from '../util/durations';
|
import * as durations from '../util/durations';
|
||||||
|
@ -579,6 +580,11 @@ export default class MessageReceiver
|
||||||
handler: (ev: EnvelopeEvent) => void
|
handler: (ev: EnvelopeEvent) => void
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
|
public override addEventListener(
|
||||||
|
name: 'storyRecipientUpdate',
|
||||||
|
handler: (ev: StoryRecipientUpdateEvent) => void
|
||||||
|
): void;
|
||||||
|
|
||||||
public override addEventListener(name: string, handler: EventHandler): void {
|
public override addEventListener(name: string, handler: EventHandler): void {
|
||||||
return super.addEventListener(name, handler);
|
return super.addEventListener(name, handler);
|
||||||
}
|
}
|
||||||
|
@ -1821,7 +1827,8 @@ export default class MessageReceiver
|
||||||
const ev = new SentEvent(
|
const ev = new SentEvent(
|
||||||
{
|
{
|
||||||
destination: dropNull(destination),
|
destination: dropNull(destination),
|
||||||
destinationUuid: dropNull(destinationUuid),
|
destinationUuid:
|
||||||
|
dropNull(destinationUuid) || envelope.destinationUuid.toString(),
|
||||||
timestamp: timestamp?.toNumber(),
|
timestamp: timestamp?.toNumber(),
|
||||||
serverTimestamp: envelope.serverTimestamp,
|
serverTimestamp: envelope.serverTimestamp,
|
||||||
device: envelope.sourceDevice,
|
device: envelope.sourceDevice,
|
||||||
|
@ -1931,7 +1938,7 @@ export default class MessageReceiver
|
||||||
|
|
||||||
isAllowedToReply.set(
|
isAllowedToReply.set(
|
||||||
destinationUuid,
|
destinationUuid,
|
||||||
Boolean(recipient.isAllowedToReply)
|
recipient.isAllowedToReply !== false
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2572,6 +2579,18 @@ export default class MessageReceiver
|
||||||
return;
|
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) {
|
if (!sentMessage || !sentMessage.message) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'MessageReceiver.handleSyncMessage: sync sent message was missing message'
|
'MessageReceiver.handleSyncMessage: sync sent message was missing message'
|
||||||
|
|
|
@ -1232,8 +1232,9 @@ export default class MessageSender {
|
||||||
isUpdate,
|
isUpdate,
|
||||||
urgent,
|
urgent,
|
||||||
options,
|
options,
|
||||||
|
storyMessageRecipients,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
encodedDataMessage: Uint8Array;
|
encodedDataMessage?: Uint8Array;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
destination: string | undefined;
|
destination: string | undefined;
|
||||||
destinationUuid: string | null | undefined;
|
destinationUuid: string | null | undefined;
|
||||||
|
@ -1243,13 +1244,21 @@ export default class MessageSender {
|
||||||
isUpdate?: boolean;
|
isUpdate?: boolean;
|
||||||
urgent: boolean;
|
urgent: boolean;
|
||||||
options?: SendOptionsType;
|
options?: SendOptionsType;
|
||||||
|
storyMessageRecipients?: Array<{
|
||||||
|
destinationUuid: string;
|
||||||
|
distributionListIds: Array<string>;
|
||||||
|
isAllowedToReply: boolean;
|
||||||
|
}>;
|
||||||
}>): Promise<CallbackResultType> {
|
}>): Promise<CallbackResultType> {
|
||||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||||
|
|
||||||
const dataMessage = Proto.DataMessage.decode(encodedDataMessage);
|
|
||||||
const sentMessage = new Proto.SyncMessage.Sent();
|
const sentMessage = new Proto.SyncMessage.Sent();
|
||||||
sentMessage.timestamp = Long.fromNumber(timestamp);
|
sentMessage.timestamp = Long.fromNumber(timestamp);
|
||||||
sentMessage.message = dataMessage;
|
|
||||||
|
if (encodedDataMessage) {
|
||||||
|
const dataMessage = Proto.DataMessage.decode(encodedDataMessage);
|
||||||
|
sentMessage.message = dataMessage;
|
||||||
|
}
|
||||||
if (destination) {
|
if (destination) {
|
||||||
sentMessage.destination = destination;
|
sentMessage.destination = destination;
|
||||||
}
|
}
|
||||||
|
@ -1261,6 +1270,19 @@ export default class MessageSender {
|
||||||
expirationStartTimestamp
|
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) {
|
if (isUpdate) {
|
||||||
sentMessage.isRecipientUpdate = true;
|
sentMessage.isRecipientUpdate = true;
|
||||||
|
|
|
@ -418,3 +418,18 @@ export class ViewSyncEvent extends ConfirmableEvent {
|
||||||
super('viewSync', confirm);
|
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…
Add table
Add a link
Reference in a new issue