signal-desktop/ts/util/sendStoryMessage.ts

359 lines
11 KiB
TypeScript
Raw Normal View History

2022-08-02 19:31:55 +00:00
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
2022-08-04 19:23:24 +00:00
import type { AttachmentType } from '../types/Attachment';
2022-08-02 19:31:55 +00:00
import type { MessageAttributesType } from '../model-types.d';
import type { SendStateByConversationId } from '../messages/MessageSendState';
import type { UUIDStringType } from '../types/UUID';
import * as log from '../logging/log';
import dataInterface from '../sql/Client';
import { MY_STORY_ID, StorySendMode } from '../types/Stories';
import { getStoriesBlocked } from './stories';
2022-08-02 19:31:55 +00:00
import { ReadStatus } from '../messages/MessageReadStatus';
import { SeenStatus } from '../MessageSeenStatus';
import { SendStatus } from '../messages/MessageSendState';
import { UUID } from '../types/UUID';
import {
conversationJobQueue,
conversationQueueJobEnum,
} from '../jobs/conversationJobQueue';
2022-08-09 03:26:21 +00:00
import { getRecipients } from './getRecipients';
2022-08-02 19:31:55 +00:00
import { getSignalConnections } from './getSignalConnections';
import { incrementMessageCounter } from './incrementMessageCounter';
2022-08-09 03:26:21 +00:00
import { isGroupV2 } from './whatTypeOfConversation';
2022-08-02 19:31:55 +00:00
import { isNotNil } from './isNotNil';
import { collect } from './iterables';
2022-11-16 20:18:02 +00:00
import { DurationInSeconds } from './durations';
2022-08-02 19:31:55 +00:00
export async function sendStoryMessage(
listIds: Array<string>,
2022-08-09 03:26:21 +00:00
conversationIds: Array<string>,
2022-08-04 19:23:24 +00:00
attachment: AttachmentType
2022-08-02 19:31:55 +00:00
): Promise<void> {
if (getStoriesBlocked()) {
log.warn('stories.sendStoryMessage: stories disabled, returning early');
return;
}
2022-08-02 19:31:55 +00:00
const { messaging } = window.textsecure;
if (!messaging) {
log.warn(
'stories.sendStoryMessage: messaging not available, returning early'
);
2022-08-02 19:31:55 +00:00
return;
}
const distributionLists = (
await Promise.all(
listIds.map(listId =>
dataInterface.getStoryDistributionWithMembers(listId)
)
)
).filter(isNotNil);
2022-08-09 03:26:21 +00:00
if (!distributionLists.length && !conversationIds.length) {
log.warn(
'stories.sendStoryMessage: Dropping send. no conversations to send to and no distribution lists found for',
2022-08-02 19:31:55 +00:00
listIds
);
return;
}
const ourConversation =
window.ConversationController.getOurConversationOrThrow();
const timestamp = Date.now();
const sendStateByListId = new Map<
UUIDStringType,
SendStateByConversationId
>();
const recipientsAlreadySentTo = new Map<UUIDStringType, boolean>();
// * Create the custom sendStateByConversationId for each distribution list
// * De-dupe members to make sure they're only sent to once
// * Figure out who can reply/who can't
distributionLists
.sort(list => (list.allowsReplies ? -1 : 1))
.forEach(distributionList => {
const sendStateByConversationId: SendStateByConversationId = {};
let distributionListMembers: Array<UUIDStringType> = [];
if (distributionList.id === MY_STORY_ID && distributionList.isBlockList) {
2022-08-02 19:31:55 +00:00
const inBlockList = new Set<UUIDStringType>(distributionList.members);
distributionListMembers = getSignalConnections().reduce(
(acc, convo) => {
const id = convo.get('uuid');
if (!id) {
return acc;
}
const uuid = UUID.cast(id);
2022-08-02 19:31:55 +00:00
if (inBlockList.has(uuid)) {
return acc;
}
if (convo.isEverUnregistered()) {
return acc;
}
2022-08-02 19:31:55 +00:00
acc.push(uuid);
return acc;
},
[] as Array<UUIDStringType>
);
} else {
distributionListMembers = distributionList.members;
}
distributionListMembers.forEach(destinationUuid => {
const conversation = window.ConversationController.get(destinationUuid);
if (!conversation) {
return;
}
sendStateByConversationId[conversation.id] = {
isAllowedToReplyToStory:
recipientsAlreadySentTo.get(destinationUuid) ||
distributionList.allowsReplies,
isAlreadyIncludedInAnotherDistributionList:
recipientsAlreadySentTo.has(destinationUuid),
status: SendStatus.Pending,
updatedAt: timestamp,
};
if (!recipientsAlreadySentTo.has(destinationUuid)) {
recipientsAlreadySentTo.set(
destinationUuid,
distributionList.allowsReplies
);
}
});
sendStateByListId.set(distributionList.id, sendStateByConversationId);
});
2022-08-04 19:23:24 +00:00
const attachments: Array<AttachmentType> = [attachment];
2022-08-02 19:31:55 +00:00
// * Gather all the job data we'll be sending to the sendStory job
// * Create the message for each distribution list
2022-08-09 03:26:21 +00:00
const distributionListMessages: Array<MessageAttributesType> =
await Promise.all(
distributionLists.map(async distributionList => {
const sendStateByConversationId = sendStateByListId.get(
distributionList.id
);
if (!sendStateByConversationId) {
log.warn(
'stories.sendStoryMessage: No sendStateByConversationId for distribution list',
distributionList.id
);
}
// Note: we use the same sent_at for these messages because we want de-duplication
// on the receiver side.
2022-08-09 03:26:21 +00:00
return window.Signal.Migrations.upgradeMessageSchema({
attachments,
conversationId: ourConversation.id,
2022-11-16 20:18:02 +00:00
expireTimer: DurationInSeconds.DAY,
expirationStartTimestamp: Date.now(),
2022-08-09 03:26:21 +00:00
id: UUID.generate().toString(),
readStatus: ReadStatus.Read,
received_at: incrementMessageCounter(),
received_at_ms: timestamp,
seenStatus: SeenStatus.NotApplicable,
sendStateByConversationId,
sent_at: timestamp,
source: window.textsecure.storage.user.getNumber(),
sourceUuid: window.textsecure.storage.user.getUuid()?.toString(),
sourceDevice: window.textsecure.storage.user.getDeviceId(),
2022-08-09 03:26:21 +00:00
storyDistributionListId: distributionList.id,
timestamp,
type: 'story',
});
})
);
const groupV2MessagesByConversationId = new Map<
string,
MessageAttributesType
>();
const groupsToSendTo = Array.from(
collect(conversationIds, conversationId => {
2022-08-09 03:26:21 +00:00
const group = window.ConversationController.get(conversationId);
2022-08-02 19:31:55 +00:00
2022-08-09 03:26:21 +00:00
if (!group) {
2022-08-02 19:31:55 +00:00
log.warn(
2022-08-09 03:26:21 +00:00
'stories.sendStoryMessage: No group found for id',
conversationId
2022-08-02 19:31:55 +00:00
);
2022-08-09 03:26:21 +00:00
return;
2022-08-02 19:31:55 +00:00
}
2022-08-09 03:26:21 +00:00
if (!isGroupV2(group.attributes)) {
log.warn(
'stories.sendStoryMessage: Conversation we tried to send to is not a groupV2',
conversationId
);
return;
}
if (group.get('announcementsOnly') && !group.areWeAdmin()) {
log.warn(
'stories.sendStoryMessage: cannot send to an announcement only group as a non-admin',
conversationId
);
return;
}
return group;
})
);
// sending a story to a group marks it as one we want to always
// include on the send-story-to list
const groupsToUpdate = Array.from(groupsToSendTo).filter(
group => group.getStorySendMode() !== StorySendMode.Always
);
for (const group of groupsToUpdate) {
group.set('storySendMode', StorySendMode.Always);
}
void window.Signal.Data.updateConversations(
groupsToUpdate.map(group => group.attributes)
);
for (const group of groupsToUpdate) {
group.captureChange('storySendMode');
}
await Promise.all(
groupsToSendTo.map(async (group, index) => {
// We want all of these timestamps to be different from the My Story timestamp.
const groupTimestamp = timestamp + index + 1;
2022-08-09 03:26:21 +00:00
const myId = window.ConversationController.getOurConversationIdOrThrow();
const sendState = {
status: SendStatus.Pending,
updatedAt: groupTimestamp,
2022-08-09 03:26:21 +00:00
};
const sendStateByConversationId = getRecipients(group.attributes).reduce(
(acc, id) => {
const conversation = window.ConversationController.get(id);
if (!conversation) {
return acc;
}
return {
...acc,
[conversation.id]: sendState,
};
},
{
[myId]: sendState,
}
);
const messageAttributes =
await window.Signal.Migrations.upgradeMessageSchema({
attachments,
2022-08-25 16:10:56 +00:00
canReplyToStory: true,
conversationId: group.id,
2022-11-16 20:18:02 +00:00
expireTimer: DurationInSeconds.DAY,
expirationStartTimestamp: Date.now(),
2022-08-09 03:26:21 +00:00
id: UUID.generate().toString(),
readStatus: ReadStatus.Read,
received_at: incrementMessageCounter(),
received_at_ms: groupTimestamp,
2022-08-09 03:26:21 +00:00
seenStatus: SeenStatus.NotApplicable,
sendStateByConversationId,
sent_at: groupTimestamp,
2022-08-09 03:26:21 +00:00
source: window.textsecure.storage.user.getNumber(),
sourceUuid: window.textsecure.storage.user.getUuid()?.toString(),
sourceDevice: window.textsecure.storage.user.getDeviceId(),
timestamp: groupTimestamp,
2022-08-09 03:26:21 +00:00
type: 'story',
});
groupV2MessagesByConversationId.set(group.id, messageAttributes);
2022-08-02 19:31:55 +00:00
})
);
2022-08-09 03:26:21 +00:00
// For distribution lists:
2022-08-02 19:31:55 +00:00
// * Save the message model
// * Add the message to the conversation
await Promise.all(
2022-08-09 03:26:21 +00:00
distributionListMessages.map(messageAttributes => {
2022-08-02 19:31:55 +00:00
const model = new window.Whisper.Message(messageAttributes);
const message = window.MessageController.register(model.id, model);
void ourConversation.addSingleMessage(model, { isJustSent: true });
2022-08-02 19:31:55 +00:00
log.info(
`stories.sendStoryMessage: saving message ${messageAttributes.timestamp}`
);
2022-08-02 19:31:55 +00:00
return dataInterface.saveMessage(message.attributes, {
forceSave: true,
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
});
})
);
2022-08-09 03:26:21 +00:00
// * Send to the distribution lists
2022-08-02 19:31:55 +00:00
// * Place into job queue
// * Save the job
await conversationJobQueue.add({
type: conversationQueueJobEnum.enum.Story,
conversationId: ourConversation.id,
messageIds: distributionListMessages.map(m => m.id),
timestamp,
});
2022-08-09 03:26:21 +00:00
// * Send to groups
// * Save the message models
// * Add message to group conversation
await Promise.all(
conversationIds.map(conversationId => {
const messageAttributes =
groupV2MessagesByConversationId.get(conversationId);
if (!messageAttributes) {
log.warn(
'stories.sendStoryMessage: Trying to send a group story but it did not exist? This is unexpected. Not sending.',
conversationId
);
return;
}
return conversationJobQueue.add(
{
type: conversationQueueJobEnum.enum.Story,
conversationId,
messageIds: [messageAttributes.id],
// using the group timestamp, which will differ from the 1:1 timestamp
timestamp: messageAttributes.timestamp,
2022-08-09 03:26:21 +00:00
},
async jobToInsert => {
const model = new window.Whisper.Message(messageAttributes);
const message = window.MessageController.register(model.id, model);
const conversation = message.getConversation();
void conversation?.addSingleMessage(model, { isJustSent: true });
2022-08-09 03:26:21 +00:00
log.info(
`stories.sendStoryMessage: saving message ${messageAttributes.timestamp}`
);
2022-08-09 03:26:21 +00:00
await dataInterface.saveMessage(message.attributes, {
forceSave: true,
jobToInsert,
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
});
}
);
})
);
2022-08-02 19:31:55 +00:00
}