209 lines
6.9 KiB
TypeScript
209 lines
6.9 KiB
TypeScript
// 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 { DeleteModel } from '../messageModifiers/Deletes';
|
|
import { SendStatus } from '../messages/MessageSendState';
|
|
import { getConversationIdForLogging } from './idForLogging';
|
|
import { isStory } from '../state/selectors/message';
|
|
import { normalizeUuid } from './normalizeUuid';
|
|
import { queueUpdateMessage } from './messageBatcher';
|
|
import { isMe } from './whatTypeOfConversation';
|
|
import { drop } from './drop';
|
|
|
|
export async function onStoryRecipientUpdate(
|
|
event: StoryRecipientUpdateEvent
|
|
): Promise<void> {
|
|
const { data, confirm } = event;
|
|
|
|
const { destinationUuid, timestamp } = data;
|
|
|
|
const conversation = window.ConversationController.get(destinationUuid);
|
|
|
|
const logId = `onStoryRecipientUpdate(${destinationUuid}, ${timestamp})`;
|
|
|
|
if (!conversation) {
|
|
log.warn(`${logId}: no conversation`);
|
|
return;
|
|
}
|
|
|
|
if (!isMe(conversation.attributes)) {
|
|
log.warn(`${logId}: story recipient update on invalid conversation`);
|
|
return;
|
|
}
|
|
|
|
const targetConversation =
|
|
await window.ConversationController.getConversationForTargetMessage(
|
|
conversation.id,
|
|
timestamp
|
|
);
|
|
|
|
if (!targetConversation) {
|
|
log.info(`${logId}: no targetConversation`);
|
|
return;
|
|
}
|
|
|
|
drop(
|
|
targetConversation.queueJob(logId, async () => {
|
|
log.info(`${logId}: updating`);
|
|
|
|
// Build up some maps for fast/easy lookups
|
|
const isAllowedToReply = new Map<string, boolean>();
|
|
const distributionListIdToConversationIds = new Map<
|
|
string,
|
|
Set<string>
|
|
>();
|
|
data.storyMessageRecipients.forEach(item => {
|
|
const { destinationAci, destinationPni } = item;
|
|
|
|
const destinationId = destinationAci || destinationPni;
|
|
if (!destinationId) {
|
|
return;
|
|
}
|
|
|
|
const convo = window.ConversationController.get(
|
|
normalizeUuid(destinationId, `${logId}.destinationId`)
|
|
);
|
|
|
|
if (!convo || !item.distributionListIds) {
|
|
return;
|
|
}
|
|
|
|
for (const rawUuid of item.distributionListIds) {
|
|
const uuid = normalizeUuid(rawUuid, `${logId}.distributionListId`);
|
|
|
|
const existing = distributionListIdToConversationIds.get(uuid);
|
|
if (existing === undefined) {
|
|
distributionListIdToConversationIds.set(uuid, new Set([convo.id]));
|
|
} else {
|
|
existing.add(convo.id);
|
|
}
|
|
}
|
|
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
|
|
const handledMessages = messages.filter(item => {
|
|
if (!isStory(item)) {
|
|
return false;
|
|
}
|
|
|
|
const { sendStateByConversationId, storyDistributionListId } = item;
|
|
|
|
if (!sendStateByConversationId || !storyDistributionListId) {
|
|
return false;
|
|
}
|
|
|
|
const newConversationIds =
|
|
distributionListIdToConversationIds.get(storyDistributionListId) ??
|
|
new Set();
|
|
|
|
const nextSendStateByConversationId = {
|
|
...sendStateByConversationId,
|
|
};
|
|
|
|
// Find conversation ids present in the local send state, but missing
|
|
// in the remote state, and remove them from the local state.
|
|
for (const oldId of Object.keys(sendStateByConversationId)) {
|
|
if (!newConversationIds.has(oldId)) {
|
|
const recipient = window.ConversationController.get(oldId);
|
|
|
|
const recipientLogId = recipient
|
|
? getConversationIdForLogging(recipient.attributes)
|
|
: oldId;
|
|
|
|
log.info(`${logId}: removing`, {
|
|
recipient: recipientLogId,
|
|
messageId: item.id,
|
|
storyDistributionListId,
|
|
});
|
|
delete nextSendStateByConversationId[oldId];
|
|
}
|
|
}
|
|
|
|
// Find conversation ids present in the remote send state, but missing in
|
|
// the local send state, and add them to the local state.
|
|
for (const newId of newConversationIds) {
|
|
if (sendStateByConversationId[newId] === undefined) {
|
|
const recipient = window.ConversationController.get(newId);
|
|
|
|
const recipientLogId = recipient
|
|
? getConversationIdForLogging(recipient.attributes)
|
|
: newId;
|
|
|
|
log.info(`${logId}: adding`, {
|
|
recipient: recipientLogId,
|
|
messageId: item.id,
|
|
storyDistributionListId,
|
|
});
|
|
nextSendStateByConversationId[newId] = {
|
|
isAllowedToReplyToStory: Boolean(isAllowedToReply.get(newId)),
|
|
status: SendStatus.Sent,
|
|
updatedAt: now,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (isEqual(sendStateByConversationId, nextSendStateByConversationId)) {
|
|
log.info(`${logId}: sendStateByConversationId does not need update`, {
|
|
messageId: item.id,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
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(`${logId} DOE`, {
|
|
messageId: item.id,
|
|
storyDistributionListId,
|
|
});
|
|
const delAttributes: DeleteAttributesType = {
|
|
fromId: ourConversationId,
|
|
serverTimestamp: Number(item.serverTimestamp),
|
|
targetSentTimestamp: item.timestamp,
|
|
};
|
|
const doe = new DeleteModel(delAttributes);
|
|
|
|
// There are no longer any remaining members for this message so lets
|
|
// run it through deleteForEveryone which marks the message as
|
|
// deletedForEveryone locally.
|
|
//
|
|
// NOTE: We don't call `Deletes.onDelete()` so the message lookup by
|
|
// sent timestamp doesn't happen (it would return all copies of the
|
|
// story, not just the one we want to delete).
|
|
void message.handleDeleteForEveryone(doe);
|
|
} else {
|
|
message.set({
|
|
sendStateByConversationId: nextSendStateByConversationId,
|
|
});
|
|
queueUpdateMessage(message.attributes);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
if (handledMessages.length) {
|
|
window.Whisper.events.trigger('incrementProgress');
|
|
confirm();
|
|
}
|
|
})
|
|
);
|
|
}
|