diff --git a/ts/background.ts b/ts/background.ts index 3d72fc448..117ebe937 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -228,6 +228,7 @@ export async function startApp(): Promise { hasStoriesDisabled: window.storage.get('hasStoriesDisabled', false), }); window.textsecure.server = server; + window.textsecure.messaging = new window.textsecure.MessageSender(server); initializeAllJobQueues({ server, @@ -2084,8 +2085,6 @@ export async function startApp(): Promise { return; } - window.textsecure.messaging = new window.textsecure.MessageSender(server); - // Update our profile key in the conversation if we just got linked. const profileKey = await ourProfileKeyService.get(); if (firstRun && profileKey) { diff --git a/ts/jobs/helpers/sendStory.ts b/ts/jobs/helpers/sendStory.ts index 98512ec54..15acb6977 100644 --- a/ts/jobs/helpers/sendStory.ts +++ b/ts/jobs/helpers/sendStory.ts @@ -34,6 +34,7 @@ import { isNotNil } from '../../util/isNotNil'; import { isSent } from '../../messages/MessageSendState'; import { ourProfileKeyService } from '../../services/ourProfileKey'; import { sendContentMessageToGroup } from '../../util/sendToGroup'; +import { SendMessageChallengeError } from '../../textsecure/Errors'; export async function sendStory( conversation: ConversationModel, @@ -55,6 +56,16 @@ export async function sendStory( return; } + // We can send a story to either: + // 1) the current group, or + // 2) all selected distribution lists (in queue for our own conversationId) + if (!isGroupV2(conversation.attributes) && !isMe(conversation.attributes)) { + log.error( + 'stories.sendStory: Conversation is neither groupV2 nor our own. Cannot send.' + ); + return; + } + // We want to generate the StoryMessage proto once at the top level so we // can reuse it but first we'll need textAttachment | fileAttachment. // This function pulls off the attachment and generates the proto from the @@ -153,9 +164,11 @@ export async function sendStory( let isSyncMessageUpdate = false; - // Send to all distribution lists - await Promise.all( - messageIds.map(async messageId => { + // Note: We capture errors here so we are sure to wait for every send process to + // complete, and so we can send a sync message afterwards if we sent the story + // successfully to at least one recipient. + const sendResults = await Promise.allSettled( + messageIds.map(async (messageId: string): Promise => { const message = await getMessageById(messageId); if (!message) { log.info( @@ -322,9 +335,8 @@ export async function sendStory( urgent: false, }); - // Do not send sync messages for distribution lists since that's sent - // in bulk at the end. - message.doNotSendSyncMessage = Boolean(distributionList); + // Don't send normal sync messages; a story sync is sent at the end of the process + message.doNotSendSyncMessage = true; const messageSendPromise = message.send( handleMessageSend(innerPromise, { @@ -391,6 +403,26 @@ export async function sendStory( } } catch (thrownError: unknown) { const errors = [thrownError, ...messageSendErrors]; + + // We need to check for this here because we can only throw one error up to + // conversationJobQueue. + errors.forEach(error => { + if (error instanceof SendMessageChallengeError) { + window.Signal.challengeHandler?.register( + { + conversationId: conversation.id, + createdAt: Date.now(), + retryAt: error.retryAt, + token: error.data?.token, + reason: + 'conversationJobQueue.run(' + + `${conversation.idForLogging()}, story, ${timestamp})`, + }, + error.data + ); + } + }); + await handleMultipleSendErrors({ errors, isFinalAttempt, @@ -470,21 +502,39 @@ export async function sendStory( }); }); - const options = await getSendOptions(conversation.attributes, { - syncMessage: true, - }); + if (storyMessageRecipients.length === 0) { + log.warn( + 'No successful sends; will not send a sync message for this attempt' + ); + } else { + const options = await getSendOptions(conversation.attributes, { + syncMessage: true, + }); - messaging.sendSyncMessage({ - destination: conversation.get('e164'), - destinationUuid: conversation.get('uuid'), - storyMessage: originalStoryMessage, - storyMessageRecipients, - expirationStartTimestamp: null, - isUpdate: isSyncMessageUpdate, - options, - timestamp, - urgent: false, + await messaging.sendSyncMessage({ + // Note: these two fields will be undefined if we're sending to a group + destination: conversation.get('e164'), + destinationUuid: conversation.get('uuid'), + storyMessage: originalStoryMessage, + storyMessageRecipients, + expirationStartTimestamp: null, + isUpdate: isSyncMessageUpdate, + options, + timestamp, + urgent: false, + }); + } + + // We can only throw one Error up to conversationJobQueue to fail the send + const sendErrors: Array = []; + sendResults.forEach(result => { + if (result.status === 'rejected') { + sendErrors.push(result); + } }); + if (sendErrors.length) { + throw sendErrors[0].reason; + } } function getMessageRecipients({ diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 6b59e5d04..d2ea61c5b 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -548,7 +548,9 @@ export const getNonGroupStories = createSelector( conversationIdsWithStories: Set ): Array => { return groups.filter( - group => !isGroupInStoryMode(group, conversationIdsWithStories) + group => + !isGroupV2(group) || + !isGroupInStoryMode(group, conversationIdsWithStories) ); } ); @@ -560,8 +562,10 @@ export const getGroupStories = createSelector( conversationLookup: ConversationLookupType, conversationIdsWithStories: Set ): Array => { - return Object.values(conversationLookup).filter(conversation => - isGroupInStoryMode(conversation, conversationIdsWithStories) + return Object.values(conversationLookup).filter( + conversation => + isGroupV2(conversation) && + isGroupInStoryMode(conversation, conversationIdsWithStories) ); } ); diff --git a/ts/util/sendDeleteForEveryoneMessage.ts b/ts/util/sendDeleteForEveryoneMessage.ts index 0ebd6565f..d91d875b6 100644 --- a/ts/util/sendDeleteForEveryoneMessage.ts +++ b/ts/util/sendDeleteForEveryoneMessage.ts @@ -40,11 +40,9 @@ export async function sendDeleteForEveryoneMessage( const messageModel = window.MessageController.register(messageId, message); const timestamp = Date.now(); - if ( - timestamp - targetTimestamp > - (deleteForEveryoneDuration || THREE_HOURS) - ) { - throw new Error('Cannot send DOE for a message older than three hours'); + const maxDuration = deleteForEveryoneDuration || THREE_HOURS; + if (timestamp - targetTimestamp > maxDuration) { + throw new Error(`Cannot send DOE for a message older than ${maxDuration}`); } messageModel.set({ diff --git a/ts/util/sendStoryMessage.ts b/ts/util/sendStoryMessage.ts index 0d302c2c4..50152cf8f 100644 --- a/ts/util/sendStoryMessage.ts +++ b/ts/util/sendStoryMessage.ts @@ -156,6 +156,7 @@ export async function sendStoryMessage( attachments, conversationId: ourConversation.id, expireTimer: DAY / SECOND, + expirationStartTimestamp: Date.now(), id: UUID.generate().toString(), readStatus: ReadStatus.Read, received_at: incrementMessageCounter(), @@ -205,7 +206,8 @@ export async function sendStoryMessage( return; } - const groupTimestamp = timestamp + index; + // We want all of these timestamps to be different from the My Story timestamp. + const groupTimestamp = timestamp + index + 1; const myId = window.ConversationController.getOurConversationIdOrThrow(); const sendState = { @@ -236,6 +238,7 @@ export async function sendStoryMessage( canReplyToStory: true, conversationId, expireTimer: DAY / SECOND, + expirationStartTimestamp: Date.now(), id: UUID.generate().toString(), readStatus: ReadStatus.Read, received_at: incrementMessageCounter(),