Message Send Log to enable comprehensive resend
This commit is contained in:
parent
0fe68b57b1
commit
a42c41ed01
37 changed files with 3154 additions and 1266 deletions
|
@ -613,6 +613,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"decryptionErrorToast": {
|
||||||
|
"message": "Desktop ran into a decryption error. Click to submit a debug log.",
|
||||||
|
"description": "An error popup when we haven't added an error for decryption error."
|
||||||
|
},
|
||||||
"oneNonImageAtATimeToast": {
|
"oneNonImageAtATimeToast": {
|
||||||
"message": "When including a non-image attachment, the limit is one attachment per message.",
|
"message": "When including a non-image attachment, the limit is one attachment per message.",
|
||||||
"description": "An error popup when the user has attempted to add an attachment"
|
"description": "An error popup when the user has attempted to add an attachment"
|
||||||
|
|
141
js/reactions.js
141
js/reactions.js
|
@ -1,141 +0,0 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
/* global
|
|
||||||
Backbone,
|
|
||||||
Whisper,
|
|
||||||
MessageController,
|
|
||||||
ConversationController
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function () {
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
Whisper.Reactions = new (Backbone.Collection.extend({
|
|
||||||
forMessage(message) {
|
|
||||||
if (message.isOutgoing()) {
|
|
||||||
const outgoingReactions = this.filter({
|
|
||||||
targetTimestamp: message.get('sent_at'),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (outgoingReactions.length > 0) {
|
|
||||||
window.log.info('Found early reaction for outgoing message');
|
|
||||||
this.remove(outgoingReactions);
|
|
||||||
return outgoingReactions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const senderId = message.getContactId();
|
|
||||||
const sentAt = message.get('sent_at');
|
|
||||||
const reactionsBySource = this.filter(re => {
|
|
||||||
const targetSenderId = ConversationController.ensureContactIds({
|
|
||||||
uuid: re.get('targetAuthorUuid'),
|
|
||||||
});
|
|
||||||
const targetTimestamp = re.get('targetTimestamp');
|
|
||||||
return targetSenderId === senderId && targetTimestamp === sentAt;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (reactionsBySource.length > 0) {
|
|
||||||
window.log.info('Found early reaction for message');
|
|
||||||
this.remove(reactionsBySource);
|
|
||||||
return reactionsBySource;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
async onReaction(reaction) {
|
|
||||||
try {
|
|
||||||
// The conversation the target message was in; we have to find it in the database
|
|
||||||
// to to figure that out.
|
|
||||||
const targetConversation = await ConversationController.getConversationForTargetMessage(
|
|
||||||
ConversationController.ensureContactIds({
|
|
||||||
uuid: reaction.get('targetAuthorUuid'),
|
|
||||||
}),
|
|
||||||
reaction.get('targetTimestamp')
|
|
||||||
);
|
|
||||||
if (!targetConversation) {
|
|
||||||
window.log.info(
|
|
||||||
'No target conversation for reaction',
|
|
||||||
reaction.get('targetAuthorUuid'),
|
|
||||||
reaction.get('targetTimestamp')
|
|
||||||
);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// awaiting is safe since `onReaction` is never called from inside the queue
|
|
||||||
return await targetConversation.queueJob(
|
|
||||||
'Reactions.onReaction',
|
|
||||||
async () => {
|
|
||||||
window.log.info(
|
|
||||||
'Handling reaction for',
|
|
||||||
reaction.get('targetTimestamp')
|
|
||||||
);
|
|
||||||
|
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
|
||||||
reaction.get('targetTimestamp'),
|
|
||||||
{
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// Message is fetched inside the conversation queue so we have the
|
|
||||||
// most recent data
|
|
||||||
const targetMessage = messages.find(m => {
|
|
||||||
const contact = m.getContact();
|
|
||||||
|
|
||||||
if (!contact) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mcid = contact.get('id');
|
|
||||||
const recid = ConversationController.ensureContactIds({
|
|
||||||
uuid: reaction.get('targetAuthorUuid'),
|
|
||||||
});
|
|
||||||
return mcid === recid;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!targetMessage) {
|
|
||||||
window.log.info(
|
|
||||||
'No message for reaction',
|
|
||||||
reaction.get('targetAuthorUuid'),
|
|
||||||
reaction.get('targetTimestamp')
|
|
||||||
);
|
|
||||||
|
|
||||||
// Since we haven't received the message for which we are removing a
|
|
||||||
// reaction, we can just remove those pending reactions
|
|
||||||
if (reaction.get('remove')) {
|
|
||||||
this.remove(reaction);
|
|
||||||
const oldReaction = this.where({
|
|
||||||
targetAuthorUuid: reaction.get('targetAuthorUuid'),
|
|
||||||
targetTimestamp: reaction.get('targetTimestamp'),
|
|
||||||
emoji: reaction.get('emoji'),
|
|
||||||
});
|
|
||||||
oldReaction.forEach(r => this.remove(r));
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = MessageController.register(
|
|
||||||
targetMessage.id,
|
|
||||||
targetMessage
|
|
||||||
);
|
|
||||||
|
|
||||||
const oldReaction = await message.handleReaction(reaction);
|
|
||||||
|
|
||||||
this.remove(reaction);
|
|
||||||
|
|
||||||
return oldReaction;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
'Reactions.onReaction error:',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}))();
|
|
||||||
})();
|
|
|
@ -50,6 +50,8 @@ try {
|
||||||
window.GV2_MIGRATION_DISABLE_ADD = false;
|
window.GV2_MIGRATION_DISABLE_ADD = false;
|
||||||
window.GV2_MIGRATION_DISABLE_INVITE = false;
|
window.GV2_MIGRATION_DISABLE_INVITE = false;
|
||||||
|
|
||||||
|
window.RETRY_DELAY = false;
|
||||||
|
|
||||||
window.platform = process.platform;
|
window.platform = process.platform;
|
||||||
window.getTitle = () => title;
|
window.getTitle = () => title;
|
||||||
window.getLocale = () => config.locale;
|
window.getLocale = () => config.locale;
|
||||||
|
@ -156,6 +158,10 @@ try {
|
||||||
window.log.info('shutdown');
|
window.log.info('shutdown');
|
||||||
ipc.send('shutdown');
|
ipc.send('shutdown');
|
||||||
};
|
};
|
||||||
|
window.showDebugLog = () => {
|
||||||
|
window.log.info('showDebugLog');
|
||||||
|
ipc.send('show-debug-log');
|
||||||
|
};
|
||||||
|
|
||||||
window.closeAbout = () => ipc.send('close-about');
|
window.closeAbout = () => ipc.send('close-about');
|
||||||
window.readyForUpdates = () => ipc.send('ready-for-updates');
|
window.readyForUpdates = () => ipc.send('ready-for-updates');
|
||||||
|
|
|
@ -9,16 +9,12 @@ import {
|
||||||
ConversationModelCollectionType,
|
ConversationModelCollectionType,
|
||||||
ConversationAttributesTypeType,
|
ConversationAttributesTypeType,
|
||||||
} from './model-types.d';
|
} from './model-types.d';
|
||||||
import { SendOptionsType, CallbackResultType } from './textsecure/SendMessage';
|
|
||||||
import { ConversationModel } from './models/conversations';
|
import { ConversationModel } from './models/conversations';
|
||||||
import { maybeDeriveGroupV2Id } from './groups';
|
import { maybeDeriveGroupV2Id } from './groups';
|
||||||
import { assert } from './util/assert';
|
import { assert } from './util/assert';
|
||||||
import { isValidGuid } from './util/isValidGuid';
|
import { isValidGuid } from './util/isValidGuid';
|
||||||
import { map, reduce } from './util/iterables';
|
import { map, reduce } from './util/iterables';
|
||||||
import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation';
|
import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation';
|
||||||
import { deprecated } from './util/deprecated';
|
|
||||||
import { getSendOptions } from './util/getSendOptions';
|
|
||||||
import { handleMessageSend } from './util/handleMessageSend';
|
|
||||||
|
|
||||||
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
||||||
|
|
||||||
|
@ -313,6 +309,25 @@ export class ConversationController {
|
||||||
return conversationId;
|
return conversationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOurConversationOrThrow(): ConversationModel {
|
||||||
|
const conversationId = this.getOurConversationIdOrThrow();
|
||||||
|
const conversation = this.get(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(
|
||||||
|
'getOurConversationOrThrow: Failed to fetch our own conversation'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
areWePrimaryDevice(): boolean {
|
||||||
|
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
|
||||||
|
|
||||||
|
return ourDeviceId === 1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a UUID and/or an E164, resolves to a string representing the local
|
* Given a UUID and/or an E164, resolves to a string representing the local
|
||||||
* database id of the given contact. In high trust mode, it may create new contacts,
|
* database id of the given contact. In high trust mode, it may create new contacts,
|
||||||
|
@ -730,25 +745,6 @@ export class ConversationController {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async prepareForSend(
|
|
||||||
id: string | undefined,
|
|
||||||
options?: { syncMessage?: boolean }
|
|
||||||
): Promise<{
|
|
||||||
wrap: (
|
|
||||||
promise: Promise<CallbackResultType | void | null>
|
|
||||||
) => Promise<CallbackResultType | void | null>;
|
|
||||||
sendOptions: SendOptionsType | undefined;
|
|
||||||
}> {
|
|
||||||
deprecated('prepareForSend');
|
|
||||||
// id is any valid conversation identifier
|
|
||||||
const conversation = this.get(id);
|
|
||||||
const sendOptions = conversation
|
|
||||||
? await getSendOptions(conversation.attributes, options)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return { wrap: handleMessageSend, sendOptions };
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAllGroupsInvolvingId(
|
async getAllGroupsInvolvingId(
|
||||||
conversationId: string
|
conversationId: string
|
||||||
): Promise<Array<ConversationModel>> {
|
): Promise<Array<ConversationModel>> {
|
||||||
|
|
|
@ -9,18 +9,19 @@ export type ConfigKeyType =
|
||||||
| 'desktop.disableGV1'
|
| 'desktop.disableGV1'
|
||||||
| 'desktop.groupCalling'
|
| 'desktop.groupCalling'
|
||||||
| 'desktop.gv2'
|
| 'desktop.gv2'
|
||||||
|
| 'desktop.internalUser'
|
||||||
| 'desktop.mandatoryProfileSharing'
|
| 'desktop.mandatoryProfileSharing'
|
||||||
| 'desktop.mediaQuality.levels'
|
| 'desktop.mediaQuality.levels'
|
||||||
| 'desktop.messageRequests'
|
| 'desktop.messageRequests'
|
||||||
| 'desktop.retryReceiptLifespan'
|
| 'desktop.retryReceiptLifespan'
|
||||||
| 'desktop.retryRespondMaxAge'
|
| 'desktop.retryRespondMaxAge'
|
||||||
| 'desktop.screensharing2'
|
| 'desktop.screensharing2'
|
||||||
| 'desktop.sendSenderKey'
|
| 'desktop.sendSenderKey2'
|
||||||
| 'desktop.storage'
|
| 'desktop.storage'
|
||||||
| 'desktop.storageWrite3'
|
| 'desktop.storageWrite3'
|
||||||
| 'desktop.worksAtSignal'
|
| 'desktop.worksAtSignal'
|
||||||
| 'global.groupsv2.maxGroupSize'
|
| 'global.groupsv2.groupSizeHardLimit'
|
||||||
| 'global.groupsv2.groupSizeHardLimit';
|
| 'global.groupsv2.maxGroupSize';
|
||||||
type ConfigValueType = {
|
type ConfigValueType = {
|
||||||
name: ConfigKeyType;
|
name: ConfigKeyType;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
typedArrayToArrayBuffer,
|
typedArrayToArrayBuffer,
|
||||||
} from './Crypto';
|
} from './Crypto';
|
||||||
import { assert } from './util/assert';
|
import { assert } from './util/assert';
|
||||||
|
import { handleMessageSend } from './util/handleMessageSend';
|
||||||
import { isNotNil } from './util/isNotNil';
|
import { isNotNil } from './util/isNotNil';
|
||||||
import { Zone } from './util/Zone';
|
import { Zone } from './util/Zone';
|
||||||
import { isMoreRecentThan } from './util/timestamp';
|
import { isMoreRecentThan } from './util/timestamp';
|
||||||
|
@ -590,6 +591,13 @@ export class SignalProtocolStore extends EventsMixin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clearSenderKeyStore(): Promise<void> {
|
||||||
|
if (this.senderKeys) {
|
||||||
|
this.senderKeys.clear();
|
||||||
|
}
|
||||||
|
await window.Signal.Data.removeAllSenderKeys();
|
||||||
|
}
|
||||||
|
|
||||||
// Session Queue
|
// Session Queue
|
||||||
|
|
||||||
async enqueueSessionJob<T>(
|
async enqueueSessionJob<T>(
|
||||||
|
@ -1231,7 +1239,14 @@ export class SignalProtocolStore extends EventsMixin {
|
||||||
|
|
||||||
// Send a null message with newly-created session
|
// Send a null message with newly-created session
|
||||||
const sendOptions = await getSendOptions(conversation.attributes);
|
const sendOptions = await getSendOptions(conversation.attributes);
|
||||||
await window.textsecure.messaging.sendNullMessage({ uuid }, sendOptions);
|
const result = await handleMessageSend(
|
||||||
|
window.textsecure.messaging.sendNullMessage({ uuid }, sendOptions),
|
||||||
|
{ messageIds: [], sendType: 'nullMessage' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result && result.errors && result.errors.length) {
|
||||||
|
throw result.errors[0];
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If we failed to do the session reset, then we'll allow another attempt sooner
|
// If we failed to do the session reset, then we'll allow another attempt sooner
|
||||||
// than one hour from now.
|
// than one hour from now.
|
||||||
|
|
603
ts/background.ts
603
ts/background.ts
|
@ -4,10 +4,6 @@
|
||||||
import { isNumber, noop } from 'lodash';
|
import { isNumber, noop } from 'lodash';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import {
|
|
||||||
DecryptionErrorMessage,
|
|
||||||
PlaintextContent,
|
|
||||||
} from '@signalapp/signal-client';
|
|
||||||
|
|
||||||
import MessageReceiver from './textsecure/MessageReceiver';
|
import MessageReceiver from './textsecure/MessageReceiver';
|
||||||
import { SessionResetsType, ProcessedDataMessage } from './textsecure/Types.d';
|
import { SessionResetsType, ProcessedDataMessage } from './textsecure/Types.d';
|
||||||
|
@ -17,7 +13,7 @@ import {
|
||||||
} from './model-types.d';
|
} from './model-types.d';
|
||||||
import * as Bytes from './Bytes';
|
import * as Bytes from './Bytes';
|
||||||
import { typedArrayToArrayBuffer } from './Crypto';
|
import { typedArrayToArrayBuffer } from './Crypto';
|
||||||
import { WhatIsThis } from './window.d';
|
import { WhatIsThis, DeliveryReceiptBatcherItemType } from './window.d';
|
||||||
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
|
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
|
||||||
import { SocketStatus } from './types/SocketStatus';
|
import { SocketStatus } from './types/SocketStatus';
|
||||||
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
|
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
|
||||||
|
@ -46,15 +42,11 @@ import {
|
||||||
TypingEvent,
|
TypingEvent,
|
||||||
ErrorEvent,
|
ErrorEvent,
|
||||||
DeliveryEvent,
|
DeliveryEvent,
|
||||||
DecryptionErrorEvent,
|
|
||||||
DecryptionErrorEventData,
|
|
||||||
SentEvent,
|
SentEvent,
|
||||||
SentEventData,
|
SentEventData,
|
||||||
ProfileKeyUpdateEvent,
|
ProfileKeyUpdateEvent,
|
||||||
MessageEvent,
|
MessageEvent,
|
||||||
MessageEventData,
|
MessageEventData,
|
||||||
RetryRequestEvent,
|
|
||||||
RetryRequestEventData,
|
|
||||||
ReadEvent,
|
ReadEvent,
|
||||||
ConfigurationEvent,
|
ConfigurationEvent,
|
||||||
ViewSyncEvent,
|
ViewSyncEvent,
|
||||||
|
@ -72,6 +64,7 @@ import * as universalExpireTimer from './util/universalExpireTimer';
|
||||||
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
|
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
|
||||||
import { getSendOptions } from './util/getSendOptions';
|
import { getSendOptions } from './util/getSendOptions';
|
||||||
import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff';
|
import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff';
|
||||||
|
import { handleMessageSend } from './util/handleMessageSend';
|
||||||
import { AppViewType } from './state/ducks/app';
|
import { AppViewType } from './state/ducks/app';
|
||||||
import { isIncoming } from './state/selectors/message';
|
import { isIncoming } from './state/selectors/message';
|
||||||
import { actionCreators } from './state/actions';
|
import { actionCreators } from './state/actions';
|
||||||
|
@ -89,6 +82,7 @@ import {
|
||||||
} from './types/SystemTraySetting';
|
} from './types/SystemTraySetting';
|
||||||
import * as Stickers from './types/Stickers';
|
import * as Stickers from './types/Stickers';
|
||||||
import { SignalService as Proto } from './protobuf';
|
import { SignalService as Proto } from './protobuf';
|
||||||
|
import { onRetryRequest, onDecryptionError } from './util/handleRetry';
|
||||||
|
|
||||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||||
|
|
||||||
|
@ -167,6 +161,7 @@ export async function startApp(): Promise<void> {
|
||||||
profileKeyResponseQueue.pause();
|
profileKeyResponseQueue.pause();
|
||||||
|
|
||||||
const lightSessionResetQueue = new window.PQueue();
|
const lightSessionResetQueue = new window.PQueue();
|
||||||
|
window.Signal.Services.lightSessionResetQueue = lightSessionResetQueue;
|
||||||
lightSessionResetQueue.pause();
|
lightSessionResetQueue.pause();
|
||||||
|
|
||||||
window.Whisper.deliveryReceiptQueue = new window.PQueue({
|
window.Whisper.deliveryReceiptQueue = new window.PQueue({
|
||||||
|
@ -174,57 +169,63 @@ export async function startApp(): Promise<void> {
|
||||||
timeout: 1000 * 60 * 2,
|
timeout: 1000 * 60 * 2,
|
||||||
});
|
});
|
||||||
window.Whisper.deliveryReceiptQueue.pause();
|
window.Whisper.deliveryReceiptQueue.pause();
|
||||||
window.Whisper.deliveryReceiptBatcher = window.Signal.Util.createBatcher({
|
window.Whisper.deliveryReceiptBatcher = window.Signal.Util.createBatcher<DeliveryReceiptBatcherItemType>(
|
||||||
name: 'Whisper.deliveryReceiptBatcher',
|
{
|
||||||
wait: 500,
|
name: 'Whisper.deliveryReceiptBatcher',
|
||||||
maxSize: 500,
|
wait: 500,
|
||||||
processBatch: async items => {
|
maxSize: 500,
|
||||||
const byConversationId = window._.groupBy(items, item =>
|
processBatch: async items => {
|
||||||
window.ConversationController.ensureContactIds({
|
const byConversationId = window._.groupBy(items, item =>
|
||||||
e164: item.source,
|
window.ConversationController.ensureContactIds({
|
||||||
uuid: item.sourceUuid,
|
e164: item.source,
|
||||||
})
|
uuid: item.sourceUuid,
|
||||||
);
|
})
|
||||||
const ids = Object.keys(byConversationId);
|
|
||||||
|
|
||||||
for (let i = 0, max = ids.length; i < max; i += 1) {
|
|
||||||
const conversationId = ids[i];
|
|
||||||
const timestamps = byConversationId[conversationId].map(
|
|
||||||
item => item.timestamp
|
|
||||||
);
|
);
|
||||||
|
const ids = Object.keys(byConversationId);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
for (let i = 0, max = ids.length; i < max; i += 1) {
|
||||||
const c = window.ConversationController.get(conversationId)!;
|
const conversationId = ids[i];
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
const ourItems = byConversationId[conversationId];
|
||||||
const uuid = c.get('uuid')!;
|
const timestamps = ourItems.map(item => item.timestamp);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
const messageIds = ourItems.map(item => item.messageId);
|
||||||
const e164 = c.get('e164')!;
|
|
||||||
|
|
||||||
c.queueJob('sendDeliveryReceipt', async () => {
|
const c = window.ConversationController.get(conversationId);
|
||||||
try {
|
if (!c) {
|
||||||
const {
|
window.log.warn(
|
||||||
wrap,
|
`deliveryReceiptBatcher: Conversation ${conversationId} does not exist! ` +
|
||||||
sendOptions,
|
`Will not send delivery receipts for timestamps ${timestamps}`
|
||||||
} = await window.ConversationController.prepareForSend(c.get('id'));
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await wrap(
|
|
||||||
window.textsecure.messaging.sendDeliveryReceipt({
|
|
||||||
e164,
|
|
||||||
uuid,
|
|
||||||
timestamps,
|
|
||||||
options: sendOptions,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
`Failed to send delivery receipt to ${e164}/${uuid} for timestamps ${timestamps}:`,
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
const uuid = c.get('uuid');
|
||||||
},
|
const e164 = c.get('e164');
|
||||||
});
|
|
||||||
|
c.queueJob('sendDeliveryReceipt', async () => {
|
||||||
|
try {
|
||||||
|
const sendOptions = await getSendOptions(c.attributes);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await handleMessageSend(
|
||||||
|
window.textsecure.messaging.sendDeliveryReceipt({
|
||||||
|
e164,
|
||||||
|
uuid,
|
||||||
|
timestamps,
|
||||||
|
options: sendOptions,
|
||||||
|
}),
|
||||||
|
{ messageIds, sendType: 'deliveryReceipt' }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
`Failed to send delivery receipt to ${e164}/${uuid} for timestamps ${timestamps}:`,
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (getTitleBarVisibility() === TitleBarVisibility.Hidden) {
|
if (getTitleBarVisibility() === TitleBarVisibility.Hidden) {
|
||||||
window.addEventListener('dblclick', (event: Event) => {
|
window.addEventListener('dblclick', (event: Event) => {
|
||||||
|
@ -899,25 +900,47 @@ export async function startApp(): Promise<void> {
|
||||||
window.Signal.Services.retryPlaceholders = retryPlaceholders;
|
window.Signal.Services.retryPlaceholders = retryPlaceholders;
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
const expired = await retryPlaceholders.getExpiredAndRemove();
|
const now = Date.now();
|
||||||
window.log.info(
|
const HOUR = 1000 * 60 * 60;
|
||||||
`retryPlaceholders/interval: Found ${expired.length} expired items`
|
const DAY = 24 * HOUR;
|
||||||
);
|
const oneDayAgo = now - DAY;
|
||||||
expired.forEach(item => {
|
try {
|
||||||
const { conversationId, senderUuid } = item;
|
await window.Signal.Data.deleteSentProtosOlderThan(oneDayAgo);
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
} catch (error) {
|
||||||
if (conversation) {
|
window.log.error(
|
||||||
const receivedAt = Date.now();
|
'background/onready/setInterval: Error deleting sent protos: ',
|
||||||
const receivedAtCounter = window.Signal.Util.incrementMessageCounter();
|
error && error.stack ? error.stack : error
|
||||||
conversation.queueJob('addDeliveryIssue', () =>
|
);
|
||||||
conversation.addDeliveryIssue({
|
}
|
||||||
receivedAt,
|
|
||||||
receivedAtCounter,
|
try {
|
||||||
senderUuid,
|
const expired = await retryPlaceholders.getExpiredAndRemove();
|
||||||
})
|
window.log.info(
|
||||||
|
`retryPlaceholders/interval: Found ${expired.length} expired items`
|
||||||
|
);
|
||||||
|
expired.forEach(item => {
|
||||||
|
const { conversationId, senderUuid } = item;
|
||||||
|
const conversation = window.ConversationController.get(
|
||||||
|
conversationId
|
||||||
);
|
);
|
||||||
}
|
if (conversation) {
|
||||||
});
|
const receivedAt = Date.now();
|
||||||
|
const receivedAtCounter = window.Signal.Util.incrementMessageCounter();
|
||||||
|
conversation.queueJob('addDeliveryIssue', () =>
|
||||||
|
conversation.addDeliveryIssue({
|
||||||
|
receivedAt,
|
||||||
|
receivedAtCounter,
|
||||||
|
senderUuid,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'background/onready/setInterval: Error getting expired retry placeholders: ',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
}, FIVE_MINUTES);
|
}, FIVE_MINUTES);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -1640,7 +1663,18 @@ export async function startApp(): Promise<void> {
|
||||||
|
|
||||||
function runStorageService() {
|
function runStorageService() {
|
||||||
window.Signal.Services.enableStorageService();
|
window.Signal.Services.enableStorageService();
|
||||||
window.textsecure.messaging.sendRequestKeySyncMessage();
|
|
||||||
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
window.log.warn(
|
||||||
|
'background/runStorageService: We are primary device; not sending key sync request'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessageSend(window.textsecure.messaging.sendRequestKeySyncMessage(), {
|
||||||
|
messageIds: [],
|
||||||
|
sendType: 'otherSync',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let challengeHandler: ChallengeHandler | undefined;
|
let challengeHandler: ChallengeHandler | undefined;
|
||||||
|
@ -1868,7 +1902,18 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.storage.remove('manifestVersion');
|
await window.storage.remove('manifestVersion');
|
||||||
await window.textsecure.messaging.sendRequestKeySyncMessage();
|
|
||||||
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
window.log.warn(
|
||||||
|
'onChange/desktop.storage: We are primary device; not sending key sync request'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleMessageSend(
|
||||||
|
window.textsecure.messaging.sendRequestKeySyncMessage(),
|
||||||
|
{ messageIds: [], sendType: 'otherSync' }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2275,7 +2320,7 @@ export async function startApp(): Promise<void> {
|
||||||
'gv2-3': true,
|
'gv2-3': true,
|
||||||
'gv1-migration': true,
|
'gv1-migration': true,
|
||||||
senderKey: window.Signal.RemoteConfig.isEnabled(
|
senderKey: window.Signal.RemoteConfig.isEnabled(
|
||||||
'desktop.sendSenderKey'
|
'desktop.sendSenderKey2'
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -2312,11 +2357,8 @@ export async function startApp(): Promise<void> {
|
||||||
runStorageService();
|
runStorageService();
|
||||||
});
|
});
|
||||||
|
|
||||||
const ourId = window.ConversationController.getOurConversationId();
|
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||||
const {
|
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||||
wrap,
|
|
||||||
sendOptions,
|
|
||||||
} = await window.ConversationController.prepareForSend(ourId, {
|
|
||||||
syncMessage: true,
|
syncMessage: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2328,11 +2370,19 @@ export async function startApp(): Promise<void> {
|
||||||
installed: true,
|
installed: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
wrap(
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
window.log.warn(
|
||||||
|
'background/connect: We are primary device; not sending sticker pack sync'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessageSend(
|
||||||
window.textsecure.messaging.sendStickerPackSync(
|
window.textsecure.messaging.sendStickerPackSync(
|
||||||
operations,
|
operations,
|
||||||
sendOptions
|
sendOptions
|
||||||
)
|
),
|
||||||
|
{ messageIds: [], sendType: 'otherSync' }
|
||||||
).catch(error => {
|
).catch(error => {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'Failed to send installed sticker packs via sync message',
|
'Failed to send installed sticker packs via sync message',
|
||||||
|
@ -3559,382 +3609,6 @@ export async function startApp(): Promise<void> {
|
||||||
window.log.warn('background onError: Doing nothing with incoming error');
|
window.log.warn('background onError: Doing nothing with incoming error');
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInList(
|
|
||||||
conversation: ConversationModel,
|
|
||||||
list: Array<string | undefined | null> | undefined
|
|
||||||
): boolean {
|
|
||||||
const uuid = conversation.get('uuid');
|
|
||||||
const e164 = conversation.get('e164');
|
|
||||||
const id = conversation.get('id');
|
|
||||||
|
|
||||||
if (!list) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list.includes(id)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uuid && list.includes(uuid)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e164 && list.includes(e164)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function archiveSessionOnMatch({
|
|
||||||
requesterUuid,
|
|
||||||
requesterDevice,
|
|
||||||
senderDevice,
|
|
||||||
}: RetryRequestEventData): Promise<void> {
|
|
||||||
const ourDeviceId = parseIntOrThrow(
|
|
||||||
window.textsecure.storage.user.getDeviceId(),
|
|
||||||
'archiveSessionOnMatch/getDeviceId'
|
|
||||||
);
|
|
||||||
if (ourDeviceId === senderDevice) {
|
|
||||||
const address = `${requesterUuid}.${requesterDevice}`;
|
|
||||||
window.log.info(
|
|
||||||
'archiveSessionOnMatch: Devices match, archiving session'
|
|
||||||
);
|
|
||||||
await window.textsecure.storage.protocol.archiveSession(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendDistributionMessageOrNullMessage(
|
|
||||||
options: RetryRequestEventData
|
|
||||||
): Promise<void> {
|
|
||||||
const { groupId, requesterUuid } = options;
|
|
||||||
let sentDistributionMessage = false;
|
|
||||||
window.log.info('sendDistributionMessageOrNullMessage: Starting', {
|
|
||||||
groupId: groupId ? `groupv2(${groupId})` : undefined,
|
|
||||||
requesterUuid,
|
|
||||||
});
|
|
||||||
|
|
||||||
await archiveSessionOnMatch(options);
|
|
||||||
|
|
||||||
const conversation = window.ConversationController.getOrCreate(
|
|
||||||
requesterUuid,
|
|
||||||
'private'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (groupId) {
|
|
||||||
const group = window.ConversationController.get(groupId);
|
|
||||||
const distributionId = group?.get('senderKeyInfo')?.distributionId;
|
|
||||||
|
|
||||||
if (group && distributionId) {
|
|
||||||
window.log.info(
|
|
||||||
'sendDistributionMessageOrNullMessage: Found matching group, sending sender key distribution message'
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
|
||||||
|
|
||||||
const result = await window.textsecure.messaging.sendSenderKeyDistributionMessage(
|
|
||||||
{
|
|
||||||
contentHint: ContentHint.DEFAULT,
|
|
||||||
distributionId,
|
|
||||||
groupId,
|
|
||||||
identifiers: [requesterUuid],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (result.errors && result.errors.length > 0) {
|
|
||||||
throw result.errors[0];
|
|
||||||
}
|
|
||||||
sentDistributionMessage = true;
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
'sendDistributionMessageOrNullMessage: Failed to send sender key distribution message',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sentDistributionMessage) {
|
|
||||||
window.log.info(
|
|
||||||
'sendDistributionMessageOrNullMessage: Did not send distribution message, sending null message'
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sendOptions = await getSendOptions(conversation.attributes);
|
|
||||||
const result = await window.textsecure.messaging.sendNullMessage(
|
|
||||||
{ uuid: requesterUuid },
|
|
||||||
sendOptions
|
|
||||||
);
|
|
||||||
if (result.errors && result.errors.length > 0) {
|
|
||||||
throw result.errors[0];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
'maybeSendDistributionMessage: Failed to send null message',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onRetryRequest(event: RetryRequestEvent) {
|
|
||||||
const { retryRequest } = event;
|
|
||||||
const {
|
|
||||||
requesterDevice,
|
|
||||||
requesterUuid,
|
|
||||||
senderDevice,
|
|
||||||
sentAt,
|
|
||||||
} = retryRequest;
|
|
||||||
const logId = `${requesterUuid}.${requesterDevice} ${sentAt}-${senderDevice}`;
|
|
||||||
|
|
||||||
window.log.info(`onRetryRequest/${logId}: Starting...`);
|
|
||||||
|
|
||||||
const requesterConversation = window.ConversationController.getOrCreate(
|
|
||||||
requesterUuid,
|
|
||||||
'private'
|
|
||||||
);
|
|
||||||
|
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(sentAt, {
|
|
||||||
MessageCollection: window.Whisper.MessageCollection,
|
|
||||||
});
|
|
||||||
|
|
||||||
const targetMessage = messages.find(message => {
|
|
||||||
if (message.get('sent_at') !== sentAt) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.get('type') !== 'outgoing') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isInList(requesterConversation, message.get('sent_to'))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!targetMessage) {
|
|
||||||
window.log.info(`onRetryRequest/${logId}: Did not find message`);
|
|
||||||
await sendDistributionMessageOrNullMessage(retryRequest);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetMessage.isErased()) {
|
|
||||||
window.log.info(
|
|
||||||
`onRetryRequest/${logId}: Message is erased, refusing to send again.`
|
|
||||||
);
|
|
||||||
await sendDistributionMessageOrNullMessage(retryRequest);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HOUR = 60 * 60 * 1000;
|
|
||||||
const ONE_DAY = 24 * HOUR;
|
|
||||||
let retryRespondMaxAge = ONE_DAY;
|
|
||||||
try {
|
|
||||||
retryRespondMaxAge = parseIntOrThrow(
|
|
||||||
window.Signal.RemoteConfig.getValue('desktop.retryRespondMaxAge'),
|
|
||||||
'retryRespondMaxAge'
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
window.log.warn(
|
|
||||||
`onRetryRequest/${logId}: Failed to parse integer from desktop.retryRespondMaxAge feature flag`,
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOlderThan(sentAt, retryRespondMaxAge)) {
|
|
||||||
window.log.info(
|
|
||||||
`onRetryRequest/${logId}: Message is too old, refusing to send again.`
|
|
||||||
);
|
|
||||||
await sendDistributionMessageOrNullMessage(retryRequest);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.log.info(`onRetryRequest/${logId}: Resending message`);
|
|
||||||
await archiveSessionOnMatch(retryRequest);
|
|
||||||
await targetMessage.resend(requesterUuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onDecryptionError(event: DecryptionErrorEvent) {
|
|
||||||
const { decryptionError } = event;
|
|
||||||
const { senderUuid, senderDevice, timestamp } = decryptionError;
|
|
||||||
const logId = `${senderUuid}.${senderDevice} ${timestamp}`;
|
|
||||||
|
|
||||||
window.log.info(`onDecryptionError/${logId}: Starting...`);
|
|
||||||
|
|
||||||
const conversation = window.ConversationController.getOrCreate(
|
|
||||||
senderUuid,
|
|
||||||
'private'
|
|
||||||
);
|
|
||||||
const capabilities = conversation.get('capabilities');
|
|
||||||
if (!capabilities) {
|
|
||||||
await conversation.getProfiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conversation.get('capabilities')?.senderKey) {
|
|
||||||
await requestResend(decryptionError);
|
|
||||||
} else {
|
|
||||||
await startAutomaticSessionReset(decryptionError);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.log.info(`onDecryptionError/${logId}: ...complete`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestResend(decryptionError: DecryptionErrorEventData) {
|
|
||||||
const {
|
|
||||||
cipherTextBytes,
|
|
||||||
cipherTextType,
|
|
||||||
contentHint,
|
|
||||||
groupId,
|
|
||||||
receivedAtCounter,
|
|
||||||
receivedAtDate,
|
|
||||||
senderDevice,
|
|
||||||
senderUuid,
|
|
||||||
timestamp,
|
|
||||||
} = decryptionError;
|
|
||||||
const logId = `${senderUuid}.${senderDevice} ${timestamp}`;
|
|
||||||
|
|
||||||
window.log.info(`requestResend/${logId}: Starting...`, {
|
|
||||||
cipherTextBytesLength: cipherTextBytes?.byteLength,
|
|
||||||
cipherTextType,
|
|
||||||
contentHint,
|
|
||||||
groupId: groupId ? `groupv2(${groupId})` : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 1. Find the target conversation
|
|
||||||
|
|
||||||
const group = groupId
|
|
||||||
? window.ConversationController.get(groupId)
|
|
||||||
: undefined;
|
|
||||||
const sender = window.ConversationController.getOrCreate(
|
|
||||||
senderUuid,
|
|
||||||
'private'
|
|
||||||
);
|
|
||||||
const conversation = group || sender;
|
|
||||||
|
|
||||||
// 2. Send resend request
|
|
||||||
|
|
||||||
if (!cipherTextBytes || !isNumber(cipherTextType)) {
|
|
||||||
window.log.warn(
|
|
||||||
`requestResend/${logId}: Missing cipherText information, failing over to automatic reset`
|
|
||||||
);
|
|
||||||
startAutomaticSessionReset(decryptionError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const message = DecryptionErrorMessage.forOriginal(
|
|
||||||
Buffer.from(cipherTextBytes),
|
|
||||||
cipherTextType,
|
|
||||||
timestamp,
|
|
||||||
senderDevice
|
|
||||||
);
|
|
||||||
|
|
||||||
const plaintext = PlaintextContent.from(message);
|
|
||||||
const options = await getSendOptions(conversation.attributes);
|
|
||||||
const result = await window.textsecure.messaging.sendRetryRequest({
|
|
||||||
plaintext,
|
|
||||||
options,
|
|
||||||
uuid: senderUuid,
|
|
||||||
});
|
|
||||||
if (result.errors && result.errors.length > 0) {
|
|
||||||
throw result.errors[0];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
`requestResend/${logId}: Failed to send retry request, failing over to automatic reset`,
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
startAutomaticSessionReset(decryptionError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
|
||||||
|
|
||||||
// 3. Determine how to represent this to the user. Three different options.
|
|
||||||
|
|
||||||
// We believe that it could be successfully re-sent, so we'll add a placeholder.
|
|
||||||
if (contentHint === ContentHint.RESENDABLE) {
|
|
||||||
const { retryPlaceholders } = window.Signal.Services;
|
|
||||||
assert(retryPlaceholders, 'requestResend: adding placeholder');
|
|
||||||
|
|
||||||
window.log.info(`requestResend/${logId}: Adding placeholder`);
|
|
||||||
await retryPlaceholders.add({
|
|
||||||
conversationId: conversation.get('id'),
|
|
||||||
receivedAt: receivedAtDate,
|
|
||||||
receivedAtCounter,
|
|
||||||
sentAt: timestamp,
|
|
||||||
senderUuid,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This message cannot be resent. We'll show no error and trust the other side to
|
|
||||||
// reset their session.
|
|
||||||
if (contentHint === ContentHint.IMPLICIT) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.log.warn(
|
|
||||||
`requestResend/${logId}: No content hint, adding error immediately`
|
|
||||||
);
|
|
||||||
conversation.queueJob('addDeliveryIssue', async () => {
|
|
||||||
conversation.addDeliveryIssue({
|
|
||||||
receivedAt: receivedAtDate,
|
|
||||||
receivedAtCounter,
|
|
||||||
senderUuid,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleSessionReset(senderUuid: string, senderDevice: number) {
|
|
||||||
// Postpone sending light session resets until the queue is empty
|
|
||||||
lightSessionResetQueue.add(() => {
|
|
||||||
window.textsecure.storage.protocol.lightSessionReset(
|
|
||||||
senderUuid,
|
|
||||||
senderDevice
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function startAutomaticSessionReset(
|
|
||||||
decryptionError: DecryptionErrorEventData
|
|
||||||
) {
|
|
||||||
const { senderUuid, senderDevice, timestamp } = decryptionError;
|
|
||||||
const logId = `${senderUuid}.${senderDevice} ${timestamp}`;
|
|
||||||
|
|
||||||
window.log.info(`startAutomaticSessionReset/${logId}: Starting...`);
|
|
||||||
|
|
||||||
scheduleSessionReset(senderUuid, senderDevice);
|
|
||||||
|
|
||||||
const conversationId = window.ConversationController.ensureContactIds({
|
|
||||||
uuid: senderUuid,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!conversationId) {
|
|
||||||
window.log.warn(
|
|
||||||
'onLightSessionReset: No conversation id, cannot add message to timeline'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
|
||||||
|
|
||||||
if (!conversation) {
|
|
||||||
window.log.warn(
|
|
||||||
'onLightSessionReset: No conversation, cannot add message to timeline'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const receivedAt = Date.now();
|
|
||||||
const receivedAtCounter = window.Signal.Util.incrementMessageCounter();
|
|
||||||
conversation.queueJob('addChatSessionRefreshed', async () => {
|
|
||||||
conversation.addChatSessionRefreshed({ receivedAt, receivedAtCounter });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onViewSync(ev: ViewSyncEvent) {
|
async function onViewSync(ev: ViewSyncEvent) {
|
||||||
ev.confirm();
|
ev.confirm();
|
||||||
|
|
||||||
|
@ -4025,7 +3699,13 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadReceipt(ev: ReadEvent) {
|
function onReadReceipt(ev: ReadEvent) {
|
||||||
const { envelopeTimestamp, timestamp, source, sourceUuid } = ev.read;
|
const {
|
||||||
|
envelopeTimestamp,
|
||||||
|
timestamp,
|
||||||
|
source,
|
||||||
|
sourceUuid,
|
||||||
|
sourceDevice,
|
||||||
|
} = ev.read;
|
||||||
const readAt = envelopeTimestamp;
|
const readAt = envelopeTimestamp;
|
||||||
const reader = window.ConversationController.ensureContactIds({
|
const reader = window.ConversationController.ensureContactIds({
|
||||||
e164: source,
|
e164: source,
|
||||||
|
@ -4036,6 +3716,7 @@ export async function startApp(): Promise<void> {
|
||||||
'read receipt',
|
'read receipt',
|
||||||
source,
|
source,
|
||||||
sourceUuid,
|
sourceUuid,
|
||||||
|
sourceDevice,
|
||||||
envelopeTimestamp,
|
envelopeTimestamp,
|
||||||
reader,
|
reader,
|
||||||
'for sent message',
|
'for sent message',
|
||||||
|
@ -4050,6 +3731,7 @@ export async function startApp(): Promise<void> {
|
||||||
|
|
||||||
const receipt = ReadReceipts.getSingleton().add({
|
const receipt = ReadReceipts.getSingleton().add({
|
||||||
reader,
|
reader,
|
||||||
|
readerDevice: sourceDevice,
|
||||||
timestamp,
|
timestamp,
|
||||||
readAt,
|
readAt,
|
||||||
});
|
});
|
||||||
|
@ -4198,6 +3880,7 @@ export async function startApp(): Promise<void> {
|
||||||
const receipt = DeliveryReceipts.getSingleton().add({
|
const receipt = DeliveryReceipts.getSingleton().add({
|
||||||
timestamp,
|
timestamp,
|
||||||
deliveredTo,
|
deliveredTo,
|
||||||
|
deliveredToDevice: sourceDevice,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Note: We don't wait for completion here
|
// Note: We don't wait for completion here
|
||||||
|
|
59
ts/groups.ts
59
ts/groups.ts
|
@ -69,7 +69,7 @@ import {
|
||||||
isGroupV2 as getIsGroupV2,
|
isGroupV2 as getIsGroupV2,
|
||||||
isMe,
|
isMe,
|
||||||
} from './util/whatTypeOfConversation';
|
} from './util/whatTypeOfConversation';
|
||||||
import { handleMessageSend } from './util/handleMessageSend';
|
import { handleMessageSend, SendTypesType } from './util/handleMessageSend';
|
||||||
import { getSendOptions } from './util/getSendOptions';
|
import { getSendOptions } from './util/getSendOptions';
|
||||||
import * as Bytes from './Bytes';
|
import * as Bytes from './Bytes';
|
||||||
import { SignalService as Proto } from './protobuf';
|
import { SignalService as Proto } from './protobuf';
|
||||||
|
@ -1309,9 +1309,12 @@ export async function modifyGroupV2({
|
||||||
profileKey,
|
profileKey,
|
||||||
},
|
},
|
||||||
conversation,
|
conversation,
|
||||||
contentHint: ContentHint.DEFAULT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
|
messageId: undefined,
|
||||||
sendOptions,
|
sendOptions,
|
||||||
})
|
sendType: 'groupChange',
|
||||||
|
}),
|
||||||
|
{ messageIds: [], sendType: 'groupChange' }
|
||||||
);
|
);
|
||||||
|
|
||||||
// We don't save this message; we just use it to ensure that a sync message is
|
// We don't save this message; we just use it to ensure that a sync message is
|
||||||
|
@ -1682,6 +1685,7 @@ export async function createGroupV2({
|
||||||
await wrapWithSyncMessageSend({
|
await wrapWithSyncMessageSend({
|
||||||
conversation,
|
conversation,
|
||||||
logId: `sendToGroup/${logId}`,
|
logId: `sendToGroup/${logId}`,
|
||||||
|
messageIds: [],
|
||||||
send: async () =>
|
send: async () =>
|
||||||
window.Signal.Util.sendToGroup({
|
window.Signal.Util.sendToGroup({
|
||||||
groupSendOptions: {
|
groupSendOptions: {
|
||||||
|
@ -1690,9 +1694,12 @@ export async function createGroupV2({
|
||||||
profileKey,
|
profileKey,
|
||||||
},
|
},
|
||||||
conversation,
|
conversation,
|
||||||
contentHint: ContentHint.DEFAULT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
|
messageId: undefined,
|
||||||
sendOptions,
|
sendOptions,
|
||||||
|
sendType: 'groupChange',
|
||||||
}),
|
}),
|
||||||
|
sendType: 'groupChange',
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2212,6 +2219,7 @@ export async function initiateMigrationToGroupV2(
|
||||||
await wrapWithSyncMessageSend({
|
await wrapWithSyncMessageSend({
|
||||||
conversation,
|
conversation,
|
||||||
logId: `sendToGroup/${logId}`,
|
logId: `sendToGroup/${logId}`,
|
||||||
|
messageIds: [],
|
||||||
send: async () =>
|
send: async () =>
|
||||||
// Minimal message to notify group members about migration
|
// Minimal message to notify group members about migration
|
||||||
window.Signal.Util.sendToGroup({
|
window.Signal.Util.sendToGroup({
|
||||||
|
@ -2223,9 +2231,12 @@ export async function initiateMigrationToGroupV2(
|
||||||
profileKey: ourProfileKey,
|
profileKey: ourProfileKey,
|
||||||
},
|
},
|
||||||
conversation,
|
conversation,
|
||||||
contentHint: ContentHint.DEFAULT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
|
messageId: undefined,
|
||||||
sendOptions,
|
sendOptions,
|
||||||
|
sendType: 'groupChange',
|
||||||
}),
|
}),
|
||||||
|
sendType: 'groupChange',
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2233,12 +2244,16 @@ export async function initiateMigrationToGroupV2(
|
||||||
export async function wrapWithSyncMessageSend({
|
export async function wrapWithSyncMessageSend({
|
||||||
conversation,
|
conversation,
|
||||||
logId,
|
logId,
|
||||||
|
messageIds,
|
||||||
send,
|
send,
|
||||||
|
sendType,
|
||||||
timestamp,
|
timestamp,
|
||||||
}: {
|
}: {
|
||||||
conversation: ConversationModel;
|
conversation: ConversationModel;
|
||||||
logId: string;
|
logId: string;
|
||||||
send: (sender: MessageSender) => Promise<CallbackResultType | undefined>;
|
messageIds: Array<string>;
|
||||||
|
send: (sender: MessageSender) => Promise<CallbackResultType>;
|
||||||
|
sendType: SendTypesType;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const sender = window.textsecure.messaging;
|
const sender = window.textsecure.messaging;
|
||||||
|
@ -2250,7 +2265,7 @@ export async function wrapWithSyncMessageSend({
|
||||||
|
|
||||||
let response: CallbackResultType | undefined;
|
let response: CallbackResultType | undefined;
|
||||||
try {
|
try {
|
||||||
response = await send(sender);
|
response = await handleMessageSend(send(sender), { messageIds, sendType });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (conversation.processSendResponse(error)) {
|
if (conversation.processSendResponse(error)) {
|
||||||
response = error;
|
response = error;
|
||||||
|
@ -2285,15 +2300,27 @@ export async function wrapWithSyncMessageSend({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await sender.sendSyncMessage({
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
encodedDataMessage: dataMessage,
|
window.log.warn(
|
||||||
timestamp,
|
`wrapWithSyncMessageSend/${logId}: We are primary device; not sync message`
|
||||||
destination: ourConversation.get('e164'),
|
);
|
||||||
destinationUuid: ourConversation.get('uuid'),
|
return;
|
||||||
expirationStartTimestamp: null,
|
}
|
||||||
sentTo: [],
|
|
||||||
unidentifiedDeliveries: [],
|
const options = await getSendOptions(ourConversation.attributes);
|
||||||
});
|
await handleMessageSend(
|
||||||
|
sender.sendSyncMessage({
|
||||||
|
destination: ourConversation.get('e164'),
|
||||||
|
destinationUuid: ourConversation.get('uuid'),
|
||||||
|
encodedDataMessage: dataMessage,
|
||||||
|
expirationStartTimestamp: null,
|
||||||
|
options,
|
||||||
|
sentTo: [],
|
||||||
|
timestamp,
|
||||||
|
unidentifiedDeliveries: [],
|
||||||
|
}),
|
||||||
|
{ messageIds, sendType }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function waitThenRespondToGroupV2Migration(
|
export async function waitThenRespondToGroupV2Migration(
|
||||||
|
|
|
@ -10,10 +10,15 @@ import { ConversationModel } from '../models/conversations';
|
||||||
import { MessageModel } from '../models/messages';
|
import { MessageModel } from '../models/messages';
|
||||||
import { MessageModelCollectionType } from '../model-types.d';
|
import { MessageModelCollectionType } from '../model-types.d';
|
||||||
import { isIncoming } from '../state/selectors/message';
|
import { isIncoming } from '../state/selectors/message';
|
||||||
|
import { isDirectConversation } from '../util/whatTypeOfConversation';
|
||||||
|
import dataInterface from '../sql/Client';
|
||||||
|
|
||||||
|
const { deleteSentProtoRecipient } = dataInterface;
|
||||||
|
|
||||||
type DeliveryReceiptAttributesType = {
|
type DeliveryReceiptAttributesType = {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
deliveredTo: string;
|
deliveredTo: string;
|
||||||
|
deliveredToDevice: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
class DeliveryReceiptModel extends Model<DeliveryReceiptAttributesType> {}
|
class DeliveryReceiptModel extends Model<DeliveryReceiptAttributesType> {}
|
||||||
|
@ -67,7 +72,7 @@ export class DeliveryReceipts extends Collection<DeliveryReceiptModel> {
|
||||||
message: MessageModel
|
message: MessageModel
|
||||||
): Array<DeliveryReceiptModel> {
|
): Array<DeliveryReceiptModel> {
|
||||||
let recipients: Array<string>;
|
let recipients: Array<string>;
|
||||||
if (conversation.isPrivate()) {
|
if (isDirectConversation(conversation.attributes)) {
|
||||||
recipients = [conversation.id];
|
recipients = [conversation.id];
|
||||||
} else {
|
} else {
|
||||||
recipients = conversation.getMemberIds();
|
recipients = conversation.getMemberIds();
|
||||||
|
@ -82,32 +87,29 @@ export class DeliveryReceipts extends Collection<DeliveryReceiptModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async onReceipt(receipt: DeliveryReceiptModel): Promise<void> {
|
async onReceipt(receipt: DeliveryReceiptModel): Promise<void> {
|
||||||
try {
|
const timestamp = receipt.get('timestamp');
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
const deliveredTo = receipt.get('deliveredTo');
|
||||||
receipt.get('timestamp'),
|
|
||||||
{
|
|
||||||
MessageCollection: window.Whisper.MessageCollection,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const message = await getTargetMessage(
|
try {
|
||||||
receipt.get('deliveredTo'),
|
const messages = await window.Signal.Data.getMessagesBySentAt(timestamp, {
|
||||||
messages
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
);
|
});
|
||||||
|
|
||||||
|
const message = await getTargetMessage(deliveredTo, messages);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'No message for delivery receipt',
|
'No message for delivery receipt',
|
||||||
receipt.get('deliveredTo'),
|
deliveredTo,
|
||||||
receipt.get('timestamp')
|
timestamp
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deliveries = message.get('delivered') || 0;
|
const deliveries = message.get('delivered') || 0;
|
||||||
const deliveredTo = message.get('delivered_to') || [];
|
const originalDeliveredTo = message.get('delivered_to') || [];
|
||||||
const expirationStartTimestamp = message.get('expirationStartTimestamp');
|
const expirationStartTimestamp = message.get('expirationStartTimestamp');
|
||||||
message.set({
|
message.set({
|
||||||
delivered_to: union(deliveredTo, [receipt.get('deliveredTo')]),
|
delivered_to: union(originalDeliveredTo, [deliveredTo]),
|
||||||
delivered: deliveries + 1,
|
delivered: deliveries + 1,
|
||||||
expirationStartTimestamp: expirationStartTimestamp || Date.now(),
|
expirationStartTimestamp: expirationStartTimestamp || Date.now(),
|
||||||
sent: true,
|
sent: true,
|
||||||
|
@ -126,6 +128,33 @@ export class DeliveryReceipts extends Collection<DeliveryReceiptModel> {
|
||||||
updateLeftPane();
|
updateLeftPane();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unidentifiedLookup = (
|
||||||
|
message.get('unidentifiedDeliveries') || []
|
||||||
|
).reduce((accumulator: Record<string, boolean>, identifier: string) => {
|
||||||
|
const id = window.ConversationController.getConversationId(identifier);
|
||||||
|
if (id) {
|
||||||
|
accumulator[id] = true;
|
||||||
|
}
|
||||||
|
return accumulator;
|
||||||
|
}, Object.create(null) as Record<string, boolean>);
|
||||||
|
const recipient = window.ConversationController.get(deliveredTo);
|
||||||
|
if (recipient && unidentifiedLookup[recipient.id]) {
|
||||||
|
const recipientUuid = recipient?.get('uuid');
|
||||||
|
const deviceId = receipt.get('deliveredToDevice');
|
||||||
|
|
||||||
|
if (recipientUuid && deviceId) {
|
||||||
|
await deleteSentProtoRecipient({
|
||||||
|
timestamp,
|
||||||
|
recipientUuid,
|
||||||
|
deviceId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.log.warn(
|
||||||
|
`DeliveryReceipts.onReceipt: Missing uuid or deviceId for deliveredTo ${deliveredTo}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.remove(receipt);
|
this.remove(receipt);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
|
|
|
@ -9,9 +9,14 @@ import { ConversationModel } from '../models/conversations';
|
||||||
import { MessageModel } from '../models/messages';
|
import { MessageModel } from '../models/messages';
|
||||||
import { MessageModelCollectionType } from '../model-types.d';
|
import { MessageModelCollectionType } from '../model-types.d';
|
||||||
import { isOutgoing } from '../state/selectors/message';
|
import { isOutgoing } from '../state/selectors/message';
|
||||||
|
import { isDirectConversation } from '../util/whatTypeOfConversation';
|
||||||
|
import dataInterface from '../sql/Client';
|
||||||
|
|
||||||
|
const { deleteSentProtoRecipient } = dataInterface;
|
||||||
|
|
||||||
type ReadReceiptAttributesType = {
|
type ReadReceiptAttributesType = {
|
||||||
reader: string;
|
reader: string;
|
||||||
|
readerDevice: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
readAt: number;
|
readAt: number;
|
||||||
};
|
};
|
||||||
|
@ -68,7 +73,7 @@ export class ReadReceipts extends Collection<ReadReceiptModel> {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
let ids: Array<string>;
|
let ids: Array<string>;
|
||||||
if (conversation.isPrivate()) {
|
if (isDirectConversation(conversation.attributes)) {
|
||||||
ids = [conversation.id];
|
ids = [conversation.id];
|
||||||
} else {
|
} else {
|
||||||
ids = conversation.getMemberIds();
|
ids = conversation.getMemberIds();
|
||||||
|
@ -86,29 +91,25 @@ export class ReadReceipts extends Collection<ReadReceiptModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async onReceipt(receipt: ReadReceiptModel): Promise<void> {
|
async onReceipt(receipt: ReadReceiptModel): Promise<void> {
|
||||||
try {
|
const timestamp = receipt.get('timestamp');
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
const reader = receipt.get('reader');
|
||||||
receipt.get('timestamp'),
|
|
||||||
{
|
|
||||||
MessageCollection: window.Whisper.MessageCollection,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const message = await getTargetMessage(receipt.get('reader'), messages);
|
try {
|
||||||
|
const messages = await window.Signal.Data.getMessagesBySentAt(timestamp, {
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = await getTargetMessage(reader, messages);
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
window.log.info(
|
window.log.info('No message for read receipt', reader, timestamp);
|
||||||
'No message for read receipt',
|
|
||||||
receipt.get('reader'),
|
|
||||||
receipt.get('timestamp')
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const readBy = message.get('read_by') || [];
|
const readBy = message.get('read_by') || [];
|
||||||
const expirationStartTimestamp = message.get('expirationStartTimestamp');
|
const expirationStartTimestamp = message.get('expirationStartTimestamp');
|
||||||
|
|
||||||
readBy.push(receipt.get('reader'));
|
readBy.push(reader);
|
||||||
message.set({
|
message.set({
|
||||||
read_by: readBy,
|
read_by: readBy,
|
||||||
expirationStartTimestamp: expirationStartTimestamp || Date.now(),
|
expirationStartTimestamp: expirationStartTimestamp || Date.now(),
|
||||||
|
@ -128,6 +129,22 @@ export class ReadReceipts extends Collection<ReadReceiptModel> {
|
||||||
updateLeftPane();
|
updateLeftPane();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deviceId = receipt.get('readerDevice');
|
||||||
|
const recipient = window.ConversationController.get(reader);
|
||||||
|
const recipientUuid = recipient?.get('uuid');
|
||||||
|
|
||||||
|
if (recipientUuid && deviceId) {
|
||||||
|
await deleteSentProtoRecipient({
|
||||||
|
timestamp,
|
||||||
|
recipientUuid,
|
||||||
|
deviceId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.log.warn(
|
||||||
|
`ReadReceipts.onReceipt: Missing uuid or deviceId for reader ${reader}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.remove(receipt);
|
this.remove(receipt);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
|
|
2
ts/model-types.d.ts
vendored
2
ts/model-types.d.ts
vendored
|
@ -371,5 +371,3 @@ export type ReactionAttributesType = {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
fromSync?: boolean;
|
fromSync?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare class ReactionModelType extends Backbone.Model<ReactionAttributesType> {}
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
MessageAttributesType,
|
MessageAttributesType,
|
||||||
MessageModelCollectionType,
|
MessageModelCollectionType,
|
||||||
QuotedMessageType,
|
QuotedMessageType,
|
||||||
ReactionModelType,
|
|
||||||
VerificationOptions,
|
VerificationOptions,
|
||||||
WhatIsThis,
|
WhatIsThis,
|
||||||
} from '../model-types.d';
|
} from '../model-types.d';
|
||||||
|
@ -64,7 +63,6 @@ import {
|
||||||
isGroupV2,
|
isGroupV2,
|
||||||
isMe,
|
isMe,
|
||||||
} from '../util/whatTypeOfConversation';
|
} from '../util/whatTypeOfConversation';
|
||||||
import { deprecated } from '../util/deprecated';
|
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import {
|
import {
|
||||||
hasErrors,
|
hasErrors,
|
||||||
|
@ -73,7 +71,7 @@ import {
|
||||||
getMessagePropStatus,
|
getMessagePropStatus,
|
||||||
} from '../state/selectors/message';
|
} from '../state/selectors/message';
|
||||||
import { Deletes } from '../messageModifiers/Deletes';
|
import { Deletes } from '../messageModifiers/Deletes';
|
||||||
import { Reactions } from '../messageModifiers/Reactions';
|
import { Reactions, ReactionModel } from '../messageModifiers/Reactions';
|
||||||
|
|
||||||
// TODO: remove once we move away from ArrayBuffers
|
// TODO: remove once we move away from ArrayBuffers
|
||||||
const FIXMEU8 = Uint8Array;
|
const FIXMEU8 = Uint8Array;
|
||||||
|
@ -320,11 +318,6 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isPrivate(): boolean {
|
|
||||||
deprecated('isPrivate()');
|
|
||||||
return isDirectConversation(this.attributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
isMemberRequestingToJoin(conversationId: string): boolean {
|
isMemberRequestingToJoin(conversationId: string): boolean {
|
||||||
if (!isGroupV2(this.attributes)) {
|
if (!isGroupV2(this.attributes)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -1200,7 +1193,8 @@ export class ConversationModel extends window.Backbone
|
||||||
...sendOptions,
|
...sendOptions,
|
||||||
online: true,
|
online: true,
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
{ messageIds: [], sendType: 'typing' }
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
handleMessageSend(
|
handleMessageSend(
|
||||||
|
@ -1208,11 +1202,14 @@ export class ConversationModel extends window.Backbone
|
||||||
contentHint: ContentHint.IMPLICIT,
|
contentHint: ContentHint.IMPLICIT,
|
||||||
contentMessage,
|
contentMessage,
|
||||||
conversation: this,
|
conversation: this,
|
||||||
|
messageId: undefined,
|
||||||
online: true,
|
online: true,
|
||||||
recipients: groupMembers,
|
recipients: groupMembers,
|
||||||
sendOptions,
|
sendOptions,
|
||||||
|
sendType: 'typing',
|
||||||
timestamp,
|
timestamp,
|
||||||
})
|
}),
|
||||||
|
{ messageIds: [], sendType: 'typing' }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1577,6 +1574,7 @@ export class ConversationModel extends window.Backbone
|
||||||
m => !hasErrors(m.attributes) && isIncoming(m.attributes)
|
m => !hasErrors(m.attributes) && isIncoming(m.attributes)
|
||||||
);
|
);
|
||||||
const receiptSpecs = readMessages.map(m => ({
|
const receiptSpecs = readMessages.map(m => ({
|
||||||
|
messageId: m.id,
|
||||||
senderE164: m.get('source'),
|
senderE164: m.get('source'),
|
||||||
senderUuid: m.get('sourceUuid'),
|
senderUuid: m.get('sourceUuid'),
|
||||||
senderId: window.ConversationController.ensureContactIds({
|
senderId: window.ConversationController.ensureContactIds({
|
||||||
|
@ -1988,22 +1986,22 @@ export class ConversationModel extends window.Backbone
|
||||||
// server updates were successful.
|
// server updates were successful.
|
||||||
await this.applyMessageRequestResponse(response);
|
await this.applyMessageRequestResponse(response);
|
||||||
|
|
||||||
const { ourNumber, ourUuid } = this;
|
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||||
const {
|
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||||
wrap,
|
syncMessage: true,
|
||||||
sendOptions,
|
});
|
||||||
} = await window.ConversationController.prepareForSend(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
ourNumber || ourUuid!,
|
|
||||||
{
|
|
||||||
syncMessage: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const groupId = this.getGroupIdBuffer();
|
const groupId = this.getGroupIdBuffer();
|
||||||
|
|
||||||
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
window.log.warn(
|
||||||
|
'syncMessageRequestResponse: We are primary device; not sending message request sync'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await wrap(
|
await handleMessageSend(
|
||||||
window.textsecure.messaging.syncMessageRequestResponse(
|
window.textsecure.messaging.syncMessageRequestResponse(
|
||||||
{
|
{
|
||||||
threadE164: this.get('e164'),
|
threadE164: this.get('e164'),
|
||||||
|
@ -2012,7 +2010,8 @@ export class ConversationModel extends window.Backbone
|
||||||
type: response,
|
type: response,
|
||||||
},
|
},
|
||||||
sendOptions
|
sendOptions
|
||||||
)
|
),
|
||||||
|
{ messageIds: [], sendType: 'otherSync' }
|
||||||
);
|
);
|
||||||
} catch (result) {
|
} catch (result) {
|
||||||
this.processSendResponse(result);
|
this.processSendResponse(result);
|
||||||
|
@ -2167,10 +2166,8 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
if (!options.viaSyncMessage) {
|
if (!options.viaSyncMessage) {
|
||||||
await this.sendVerifySyncMessage(
|
await this.sendVerifySyncMessage(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
this.get('e164'),
|
||||||
this.get('e164')!,
|
this.get('uuid'),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
this.get('uuid')!,
|
|
||||||
verified
|
verified
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2179,33 +2176,52 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendVerifySyncMessage(
|
async sendVerifySyncMessage(
|
||||||
e164: string,
|
e164: string | undefined,
|
||||||
uuid: string,
|
uuid: string | undefined,
|
||||||
state: number
|
state: number
|
||||||
): Promise<WhatIsThis> {
|
): Promise<CallbackResultType | void> {
|
||||||
|
const identifier = uuid || e164;
|
||||||
|
if (!identifier) {
|
||||||
|
throw new Error(
|
||||||
|
'sendVerifySyncMessage: Neither e164 nor UUID were provided'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
window.log.warn(
|
||||||
|
'sendVerifySyncMessage: We are primary device; not sending sync'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Because syncVerification sends a (null) message to the target of the verify and
|
// Because syncVerification sends a (null) message to the target of the verify and
|
||||||
// a sync message to our own devices, we need to send the accessKeys down for both
|
// a sync message to our own devices, we need to send the accessKeys down for both
|
||||||
// contacts. So we merge their sendOptions.
|
// contacts. So we merge their sendOptions.
|
||||||
const { sendOptions } = await window.ConversationController.prepareForSend(
|
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||||
this.ourNumber || this.ourUuid!,
|
syncMessage: true,
|
||||||
{ syncMessage: true }
|
});
|
||||||
);
|
|
||||||
const contactSendOptions = await getSendOptions(this.attributes);
|
const contactSendOptions = await getSendOptions(this.attributes);
|
||||||
const options = { ...sendOptions, ...contactSendOptions };
|
const options = { ...sendOptions, ...contactSendOptions };
|
||||||
|
|
||||||
const promise = window.textsecure.storage.protocol.loadIdentityKey(e164);
|
const key = await window.textsecure.storage.protocol.loadIdentityKey(
|
||||||
return promise.then(key =>
|
identifier
|
||||||
handleMessageSend(
|
);
|
||||||
window.textsecure.messaging.syncVerification(
|
if (!key) {
|
||||||
e164,
|
throw new Error(
|
||||||
uuid,
|
`sendVerifySyncMessage: No identity key found for identifier ${identifier}`
|
||||||
state,
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
}
|
||||||
key!,
|
|
||||||
options
|
await handleMessageSend(
|
||||||
)
|
window.textsecure.messaging.syncVerification(
|
||||||
)
|
e164,
|
||||||
|
uuid,
|
||||||
|
state,
|
||||||
|
key,
|
||||||
|
options
|
||||||
|
),
|
||||||
|
{ messageIds: [], sendType: 'verificationSync' }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2214,13 +2230,12 @@ export class ConversationModel extends window.Backbone
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
return this.get('verified') === this.verifiedEnum!.VERIFIED;
|
return this.get('verified') === this.verifiedEnum!.VERIFIED;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
if (!this.contactCollection!.length) {
|
if (!this.contactCollection?.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
return this.contactCollection?.every(contact => {
|
||||||
return this.contactCollection!.every(contact => {
|
|
||||||
if (isMe(contact.attributes)) {
|
if (isMe(contact.attributes)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -2238,16 +2253,12 @@ export class ConversationModel extends window.Backbone
|
||||||
verified !== this.verifiedEnum!.DEFAULT
|
verified !== this.verifiedEnum!.DEFAULT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
if (!this.contactCollection!.length) {
|
if (!this.contactCollection?.length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Array.any does not exist. This is probably broken.
|
return this.contactCollection?.some(contact => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
return this.contactCollection!.any(contact => {
|
|
||||||
if (isMe(contact.attributes)) {
|
if (isMe(contact.attributes)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -2262,8 +2273,7 @@ export class ConversationModel extends window.Backbone
|
||||||
: new window.Backbone.Collection();
|
: new window.Backbone.Collection();
|
||||||
}
|
}
|
||||||
return new window.Backbone.Collection(
|
return new window.Backbone.Collection(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
this.contactCollection?.filter(contact => {
|
||||||
this.contactCollection!.filter(contact => {
|
|
||||||
if (isMe(contact.attributes)) {
|
if (isMe(contact.attributes)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -3158,7 +3168,11 @@ export class ConversationModel extends window.Backbone
|
||||||
window.reduxActions.stickers.useSticker(packId, stickerId);
|
window.reduxActions.stickers.useSticker(packId, stickerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendDeleteForEveryoneMessage(targetTimestamp: number): Promise<void> {
|
async sendDeleteForEveryoneMessage(options: {
|
||||||
|
id: string;
|
||||||
|
timestamp: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { timestamp: targetTimestamp, id: messageId } = options;
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
|
|
||||||
if (timestamp - targetTimestamp > THREE_HOURS) {
|
if (timestamp - targetTimestamp > THREE_HOURS) {
|
||||||
|
@ -3224,7 +3238,7 @@ export class ConversationModel extends window.Backbone
|
||||||
deletedForEveryoneTimestamp: targetTimestamp,
|
deletedForEveryoneTimestamp: targetTimestamp,
|
||||||
timestamp,
|
timestamp,
|
||||||
expireTimer: undefined,
|
expireTimer: undefined,
|
||||||
contentHint: ContentHint.DEFAULT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
groupId: undefined,
|
groupId: undefined,
|
||||||
profileKey,
|
profileKey,
|
||||||
options: sendOptions,
|
options: sendOptions,
|
||||||
|
@ -3240,8 +3254,10 @@ export class ConversationModel extends window.Backbone
|
||||||
profileKey,
|
profileKey,
|
||||||
},
|
},
|
||||||
conversation: this,
|
conversation: this,
|
||||||
contentHint: ContentHint.DEFAULT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
|
messageId,
|
||||||
sendOptions,
|
sendOptions,
|
||||||
|
sendType: 'deleteForEveryone',
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
@ -3249,11 +3265,16 @@ export class ConversationModel extends window.Backbone
|
||||||
// anything to the database.
|
// anything to the database.
|
||||||
message.doNotSave = true;
|
message.doNotSave = true;
|
||||||
|
|
||||||
const result = await message.send(handleMessageSend(promise));
|
const result = await message.send(
|
||||||
|
handleMessageSend(promise, {
|
||||||
|
messageIds: [messageId],
|
||||||
|
sendType: 'deleteForEveryone',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (!message.hasSuccessfulDelivery()) {
|
if (!message.hasSuccessfulDelivery()) {
|
||||||
// This is handled by `conversation_view` which displays a toast on
|
// This is handled by `conversation_view` which displays a toast on
|
||||||
// send error.
|
// send error.
|
||||||
throw new Error('No successful delivery for delete for everyone');
|
throw new Error('No successful delivery for delete for everyone');
|
||||||
}
|
}
|
||||||
Deletes.getSingleton().onDelete(deleteModel);
|
Deletes.getSingleton().onDelete(deleteModel);
|
||||||
|
@ -3274,10 +3295,12 @@ export class ConversationModel extends window.Backbone
|
||||||
async sendReactionMessage(
|
async sendReactionMessage(
|
||||||
reaction: { emoji: string; remove: boolean },
|
reaction: { emoji: string; remove: boolean },
|
||||||
target: {
|
target: {
|
||||||
|
messageId: string;
|
||||||
targetAuthorUuid: string;
|
targetAuthorUuid: string;
|
||||||
targetTimestamp: number;
|
targetTimestamp: number;
|
||||||
}
|
}
|
||||||
): Promise<WhatIsThis> {
|
): Promise<WhatIsThis> {
|
||||||
|
const { messageId } = target;
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const outgoingReaction = { ...reaction, ...target };
|
const outgoingReaction = { ...reaction, ...target };
|
||||||
|
|
||||||
|
@ -3373,7 +3396,7 @@ export class ConversationModel extends window.Backbone
|
||||||
deletedForEveryoneTimestamp: undefined,
|
deletedForEveryoneTimestamp: undefined,
|
||||||
timestamp,
|
timestamp,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
contentHint: ContentHint.DEFAULT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
groupId: undefined,
|
groupId: undefined,
|
||||||
profileKey,
|
profileKey,
|
||||||
options,
|
options,
|
||||||
|
@ -3392,12 +3415,19 @@ export class ConversationModel extends window.Backbone
|
||||||
profileKey,
|
profileKey,
|
||||||
},
|
},
|
||||||
conversation: this,
|
conversation: this,
|
||||||
contentHint: ContentHint.DEFAULT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
|
messageId,
|
||||||
sendOptions: options,
|
sendOptions: options,
|
||||||
|
sendType: 'reaction',
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const result = await message.send(handleMessageSend(promise));
|
const result = await message.send(
|
||||||
|
handleMessageSend(promise, {
|
||||||
|
messageIds: [messageId],
|
||||||
|
sendType: 'reaction',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (!message.hasSuccessfulDelivery()) {
|
if (!message.hasSuccessfulDelivery()) {
|
||||||
// This is handled by `conversation_view` which displays a toast on
|
// This is handled by `conversation_view` which displays a toast on
|
||||||
|
@ -3407,7 +3437,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
let reverseReaction: ReactionModelType;
|
let reverseReaction: ReactionModel;
|
||||||
if (oldReaction) {
|
if (oldReaction) {
|
||||||
// Either restore old reaction
|
// Either restore old reaction
|
||||||
reverseReaction = Reactions.getSingleton().add({
|
reverseReaction = Reactions.getSingleton().add({
|
||||||
|
@ -3444,11 +3474,15 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await window.textsecure.messaging.sendProfileKeyUpdate(
|
|
||||||
profileKey,
|
await handleMessageSend(
|
||||||
recipients,
|
window.textsecure.messaging.sendProfileKeyUpdate(
|
||||||
await getSendOptions(this.attributes),
|
profileKey,
|
||||||
this.get('groupId')
|
recipients,
|
||||||
|
await getSendOptions(this.attributes),
|
||||||
|
this.get('groupId')
|
||||||
|
),
|
||||||
|
{ messageIds: [], sendType: 'profileKeyUpdate' }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3537,6 +3571,7 @@ export class ConversationModel extends window.Backbone
|
||||||
await addStickerPackReference(model.id, sticker.packId);
|
await addStickerPackReference(model.id, sticker.packId);
|
||||||
}
|
}
|
||||||
const message = window.MessageController.register(model.id, model);
|
const message = window.MessageController.register(model.id, model);
|
||||||
|
const messageId = message.id;
|
||||||
await window.Signal.Data.saveMessage(message.attributes, {
|
await window.Signal.Data.saveMessage(message.attributes, {
|
||||||
forceSave: true,
|
forceSave: true,
|
||||||
Message: window.Whisper.Message,
|
Message: window.Whisper.Message,
|
||||||
|
@ -3635,7 +3670,9 @@ export class ConversationModel extends window.Backbone
|
||||||
},
|
},
|
||||||
conversation: this,
|
conversation: this,
|
||||||
contentHint: ContentHint.RESENDABLE,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
|
messageId,
|
||||||
sendOptions: options,
|
sendOptions: options,
|
||||||
|
sendType: 'message',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
promise = window.textsecure.messaging.sendMessageToIdentifier({
|
promise = window.textsecure.messaging.sendMessageToIdentifier({
|
||||||
|
@ -3656,7 +3693,12 @@ export class ConversationModel extends window.Backbone
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return message.send(handleMessageSend(promise));
|
return message.send(
|
||||||
|
handleMessageSend(promise, {
|
||||||
|
messageIds: [messageId],
|
||||||
|
sendType: 'message',
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4099,7 +4141,12 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await message.send(handleMessageSend(promise));
|
await message.send(
|
||||||
|
handleMessageSend(promise, {
|
||||||
|
messageIds: [],
|
||||||
|
sendType: 'expirationTimerUpdate',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
@ -4220,7 +4267,8 @@ export class ConversationModel extends window.Backbone
|
||||||
groupId,
|
groupId,
|
||||||
groupIdentifiers,
|
groupIdentifiers,
|
||||||
options
|
options
|
||||||
)
|
),
|
||||||
|
{ messageIds: [], sendType: 'legacyGroupChange' }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,7 +167,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
|
|
||||||
syncPromise?: Promise<unknown>;
|
syncPromise?: Promise<CallbackResultType | void>;
|
||||||
|
|
||||||
initialize(attributes: unknown): void {
|
initialize(attributes: unknown): void {
|
||||||
if (_.isObject(attributes)) {
|
if (_.isObject(attributes)) {
|
||||||
|
@ -774,8 +774,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup(): Promise<void> {
|
async cleanup(): Promise<void> {
|
||||||
const { messageDeleted } = window.reduxActions.conversations;
|
window.reduxActions?.conversations?.messageDeleted(
|
||||||
messageDeleted(this.id, this.get('conversationId'));
|
this.id,
|
||||||
|
this.get('conversationId')
|
||||||
|
);
|
||||||
|
|
||||||
this.getConversation()?.debouncedUpdateLastMessage?.();
|
this.getConversation()?.debouncedUpdateLastMessage?.();
|
||||||
|
|
||||||
|
@ -868,26 +870,26 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = this.get('sent_at');
|
const timestamp = this.get('sent_at');
|
||||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||||
const ourUuid = window.textsecure.storage.user.getUuid()!;
|
syncMessage: true,
|
||||||
const {
|
});
|
||||||
wrap,
|
|
||||||
sendOptions,
|
|
||||||
} = await window.ConversationController.prepareForSend(
|
|
||||||
ourNumber || ourUuid,
|
|
||||||
{
|
|
||||||
syncMessage: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await wrap(
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
window.log.warn(
|
||||||
|
'markViewed: We are primary device; not sending view sync'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleMessageSend(
|
||||||
window.textsecure.messaging.syncViewOnceOpen(
|
window.textsecure.messaging.syncViewOnceOpen(
|
||||||
sender,
|
sender,
|
||||||
senderUuid,
|
senderUuid,
|
||||||
timestamp,
|
timestamp,
|
||||||
sendOptions
|
sendOptions
|
||||||
)
|
),
|
||||||
|
{ messageIds: [this.id], sendType: 'viewOnceSync' }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -987,6 +989,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
Message: window.Whisper.Message,
|
Message: window.Whisper.Message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await window.Signal.Data.deleteSentProtoByMessageId(this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEmpty(): boolean {
|
isEmpty(): boolean {
|
||||||
|
@ -1346,11 +1350,18 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
// Important to ensure that we don't consider this recipient list to be the
|
// Important to ensure that we don't consider this recipient list to be the
|
||||||
// entire member list.
|
// entire member list.
|
||||||
isPartialSend: true,
|
isPartialSend: true,
|
||||||
|
messageId: this.id,
|
||||||
sendOptions: options,
|
sendOptions: options,
|
||||||
|
sendType: 'messageRetry',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.send(handleMessageSend(promise));
|
return this.send(
|
||||||
|
handleMessageSend(promise, {
|
||||||
|
messageIds: [this.id],
|
||||||
|
sendType: 'messageRetry',
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
@ -1429,10 +1440,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
const parentConversation = this.getConversation();
|
const parentConversation = this.getConversation();
|
||||||
const groupId = parentConversation?.get('groupId');
|
const groupId = parentConversation?.get('groupId');
|
||||||
const {
|
|
||||||
wrap,
|
const recipientConversation = window.ConversationController.get(identifier);
|
||||||
sendOptions,
|
const sendOptions = recipientConversation
|
||||||
} = await window.ConversationController.prepareForSend(identifier);
|
? await getSendOptions(recipientConversation.attributes)
|
||||||
|
: undefined;
|
||||||
const group =
|
const group =
|
||||||
groupId && isGroupV1(parentConversation?.attributes)
|
groupId && isGroupV1(parentConversation?.attributes)
|
||||||
? {
|
? {
|
||||||
|
@ -1479,7 +1491,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
options: sendOptions,
|
options: sendOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.send(wrap(promise));
|
return this.send(
|
||||||
|
handleMessageSend(promise, {
|
||||||
|
messageIds: [this.id],
|
||||||
|
sendType: 'messageRetry',
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeOutgoingErrors(incomingIdentifier: string): CustomError {
|
removeOutgoingErrors(incomingIdentifier: string): CustomError {
|
||||||
|
@ -1689,18 +1706,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
// possible.
|
// possible.
|
||||||
await this.send(
|
await this.send(
|
||||||
handleMessageSend(
|
handleMessageSend(
|
||||||
// TODO: DESKTOP-724
|
|
||||||
// resetSession returns `Array<void>` which is incompatible with the
|
|
||||||
// expected promise return values. `[]` is truthy and handleMessageSend
|
|
||||||
// assumes it's a valid callback result type
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
window.textsecure.messaging.resetSession(
|
window.textsecure.messaging.resetSession(
|
||||||
options.uuid,
|
options.uuid,
|
||||||
options.e164,
|
options.e164,
|
||||||
options.now,
|
options.now,
|
||||||
sendOptions
|
sendOptions
|
||||||
)
|
),
|
||||||
|
{ messageIds: [], sendType: 'resetSession' }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1725,10 +1737,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
sent: true,
|
sent: true,
|
||||||
expirationStartTimestamp: Date.now(),
|
expirationStartTimestamp: Date.now(),
|
||||||
});
|
});
|
||||||
const result: typeof window.WhatIsThis = await this.sendSyncMessage();
|
const result = await this.sendSyncMessage();
|
||||||
this.set({
|
this.set({
|
||||||
// We have to do this afterward, since we didn't have a previous send!
|
// We have to do this afterward, since we didn't have a previous send!
|
||||||
unidentifiedDeliveries: result ? result.unidentifiedDeliveries : null,
|
unidentifiedDeliveries:
|
||||||
|
result && result.unidentifiedDeliveries
|
||||||
|
? result.unidentifiedDeliveries
|
||||||
|
: undefined,
|
||||||
|
|
||||||
// These are unique to a Note to Self message - immediately read/delivered
|
// These are unique to a Note to Self message - immediately read/delivered
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
@ -1751,30 +1766,31 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendSyncMessage(): Promise<WhatIsThis> {
|
async sendSyncMessage(): Promise<CallbackResultType | void> {
|
||||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||||
const ourUuid = window.textsecure.storage.user.getUuid();
|
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||||
const {
|
syncMessage: true,
|
||||||
wrap,
|
});
|
||||||
sendOptions,
|
|
||||||
} = await window.ConversationController.prepareForSend(
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
ourUuid || ourNumber,
|
window.log.warn(
|
||||||
{
|
'sendSyncMessage: We are primary device; not sending sync message'
|
||||||
syncMessage: true,
|
);
|
||||||
}
|
this.set({ dataMessage: undefined });
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.syncPromise = this.syncPromise || Promise.resolve();
|
this.syncPromise = this.syncPromise || Promise.resolve();
|
||||||
const next = async () => {
|
const next = async () => {
|
||||||
const dataMessage = this.get('dataMessage');
|
const dataMessage = this.get('dataMessage');
|
||||||
if (!dataMessage) {
|
if (!dataMessage) {
|
||||||
return Promise.resolve();
|
return;
|
||||||
}
|
}
|
||||||
const isUpdate = Boolean(this.get('synced'));
|
const isUpdate = Boolean(this.get('synced'));
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const conv = this.getConversation()!;
|
const conv = this.getConversation()!;
|
||||||
|
|
||||||
return wrap(
|
return handleMessageSend(
|
||||||
window.textsecure.messaging.sendSyncMessage({
|
window.textsecure.messaging.sendSyncMessage({
|
||||||
encodedDataMessage: dataMessage,
|
encodedDataMessage: dataMessage,
|
||||||
timestamp: this.get('sent_at'),
|
timestamp: this.get('sent_at'),
|
||||||
|
@ -1786,8 +1802,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
unidentifiedDeliveries: this.get('unidentifiedDeliveries') || [],
|
unidentifiedDeliveries: this.get('unidentifiedDeliveries') || [],
|
||||||
isUpdate,
|
isUpdate,
|
||||||
options: sendOptions,
|
options: sendOptions,
|
||||||
})
|
}),
|
||||||
).then(async (result: unknown) => {
|
{ messageIds: [this.id], sendType: 'sentSync' }
|
||||||
|
).then(async result => {
|
||||||
this.set({
|
this.set({
|
||||||
synced: true,
|
synced: true,
|
||||||
dataMessage: null,
|
dataMessage: null,
|
||||||
|
@ -2504,28 +2521,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now check for decryption error placeholders
|
|
||||||
const { retryPlaceholders } = window.Signal.Services;
|
|
||||||
if (retryPlaceholders) {
|
|
||||||
const item = await retryPlaceholders.findByMessageAndRemove(
|
|
||||||
conversationId,
|
|
||||||
message.get('sent_at')
|
|
||||||
);
|
|
||||||
if (item && item.wasOpened) {
|
|
||||||
window.log.info(
|
|
||||||
`handleDataMessage: found retry placeholder for ${message.idForLogging()}, but conversation was opened. No updates made.`
|
|
||||||
);
|
|
||||||
} else if (item) {
|
|
||||||
window.log.info(
|
|
||||||
`handleDataMessage: found retry placeholder for ${message.idForLogging()}. Updating received_at/received_at_ms`
|
|
||||||
);
|
|
||||||
message.set({
|
|
||||||
received_at: item.receivedAtCounter,
|
|
||||||
received_at_ms: item.receivedAt,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupV2
|
// GroupV2
|
||||||
|
|
||||||
if (initialMessage.groupV2) {
|
if (initialMessage.groupV2) {
|
||||||
|
@ -2640,6 +2635,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messageId = window.getGuid();
|
||||||
|
|
||||||
// Send delivery receipts, but only for incoming sealed sender messages
|
// Send delivery receipts, but only for incoming sealed sender messages
|
||||||
// and not for messages from unaccepted conversations
|
// and not for messages from unaccepted conversations
|
||||||
if (
|
if (
|
||||||
|
@ -2653,6 +2650,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
// The queue can be paused easily.
|
// The queue can be paused easily.
|
||||||
window.Whisper.deliveryReceiptQueue.add(() => {
|
window.Whisper.deliveryReceiptQueue.add(() => {
|
||||||
window.Whisper.deliveryReceiptBatcher.add({
|
window.Whisper.deliveryReceiptBatcher.add({
|
||||||
|
messageId,
|
||||||
source,
|
source,
|
||||||
sourceUuid,
|
sourceUuid,
|
||||||
timestamp: this.get('sent_at'),
|
timestamp: this.get('sent_at'),
|
||||||
|
@ -2689,7 +2687,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
message.set({
|
message.set({
|
||||||
id: window.getGuid(),
|
id: messageId,
|
||||||
attachments: dataMessage.attachments,
|
attachments: dataMessage.attachments,
|
||||||
body: dataMessage.body,
|
body: dataMessage.body,
|
||||||
bodyRanges: dataMessage.bodyRanges,
|
bodyRanges: dataMessage.bodyRanges,
|
||||||
|
@ -3270,6 +3268,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
conversationId: this.get('conversationId'),
|
conversationId: this.get('conversationId'),
|
||||||
emoji: reaction.get('emoji'),
|
emoji: reaction.get('emoji'),
|
||||||
fromId: reaction.get('fromId'),
|
fromId: reaction.get('fromId'),
|
||||||
|
messageId: this.id,
|
||||||
messageReceivedAt: this.get('received_at'),
|
messageReceivedAt: this.get('received_at'),
|
||||||
targetAuthorUuid: reaction.get('targetAuthorUuid'),
|
targetAuthorUuid: reaction.get('targetAuthorUuid'),
|
||||||
targetTimestamp: reaction.get('targetTimestamp'),
|
targetTimestamp: reaction.get('targetTimestamp'),
|
||||||
|
|
|
@ -57,6 +57,7 @@ import {
|
||||||
import { assert } from '../util/assert';
|
import { assert } from '../util/assert';
|
||||||
import { dropNull, shallowDropNull } from '../util/dropNull';
|
import { dropNull, shallowDropNull } from '../util/dropNull';
|
||||||
import { getOwn } from '../util/getOwn';
|
import { getOwn } from '../util/getOwn';
|
||||||
|
import { handleMessageSend } from '../util/handleMessageSend';
|
||||||
import {
|
import {
|
||||||
fetchMembershipProof,
|
fetchMembershipProof,
|
||||||
getMembershipList,
|
getMembershipList,
|
||||||
|
@ -937,13 +938,17 @@ export class CallingClass {
|
||||||
wrapWithSyncMessageSend({
|
wrapWithSyncMessageSend({
|
||||||
conversation,
|
conversation,
|
||||||
logId: `sendToGroup/groupCallUpdate/${conversationId}-${eraId}`,
|
logId: `sendToGroup/groupCallUpdate/${conversationId}-${eraId}`,
|
||||||
|
messageIds: [],
|
||||||
send: () =>
|
send: () =>
|
||||||
window.Signal.Util.sendToGroup({
|
window.Signal.Util.sendToGroup({
|
||||||
groupSendOptions: { groupCallUpdate: { eraId }, groupV2, timestamp },
|
groupSendOptions: { groupCallUpdate: { eraId }, groupV2, timestamp },
|
||||||
conversation,
|
conversation,
|
||||||
contentHint: ContentHint.DEFAULT,
|
contentHint: ContentHint.DEFAULT,
|
||||||
|
messageId: undefined,
|
||||||
sendOptions,
|
sendOptions,
|
||||||
|
sendType: 'callingMessage',
|
||||||
}),
|
}),
|
||||||
|
sendType: 'callingMessage',
|
||||||
timestamp,
|
timestamp,
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
|
@ -1559,12 +1564,19 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.textsecure.messaging.sendCallingMessage(
|
const result = await handleMessageSend(
|
||||||
remoteUserId,
|
window.textsecure.messaging.sendCallingMessage(
|
||||||
callingMessageToProto(message),
|
remoteUserId,
|
||||||
sendOptions
|
callingMessageToProto(message),
|
||||||
|
sendOptions
|
||||||
|
),
|
||||||
|
{ messageIds: [], sendType: 'callingMessage' }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (result && result.errors && result.errors.length) {
|
||||||
|
throw result.errors[0];
|
||||||
|
}
|
||||||
|
|
||||||
window.log.info('handleOutgoingSignaling() completed successfully');
|
window.log.info('handleOutgoingSignaling() completed successfully');
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {
|
||||||
import { ConversationModel } from '../models/conversations';
|
import { ConversationModel } from '../models/conversations';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
import { BackOff } from '../util/BackOff';
|
import { BackOff } from '../util/BackOff';
|
||||||
|
import { handleMessageSend } from '../util/handleMessageSend';
|
||||||
import { storageJobQueue } from '../util/JobQueue';
|
import { storageJobQueue } from '../util/JobQueue';
|
||||||
import { sleep } from '../util/sleep';
|
import { sleep } from '../util/sleep';
|
||||||
import { isMoreRecentThan } from '../util/timestamp';
|
import { isMoreRecentThan } from '../util/timestamp';
|
||||||
|
@ -531,7 +532,18 @@ async function uploadManifest(
|
||||||
window.storage.put('manifestVersion', version);
|
window.storage.put('manifestVersion', version);
|
||||||
conflictBackOff.reset();
|
conflictBackOff.reset();
|
||||||
backOff.reset();
|
backOff.reset();
|
||||||
await window.textsecure.messaging.sendFetchManifestSyncMessage();
|
|
||||||
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
window.log.warn(
|
||||||
|
'uploadManifest: We are primary device; not sending sync manifest'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleMessageSend(
|
||||||
|
window.textsecure.messaging.sendFetchManifestSyncMessage(),
|
||||||
|
{ messageIds: [], sendType: 'otherSync' }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopStorageServiceSync() {
|
async function stopStorageServiceSync() {
|
||||||
|
@ -552,7 +564,18 @@ async function stopStorageServiceSync() {
|
||||||
if (!window.textsecure.messaging) {
|
if (!window.textsecure.messaging) {
|
||||||
throw new Error('storageService.stopStorageServiceSync: We are offline!');
|
throw new Error('storageService.stopStorageServiceSync: We are offline!');
|
||||||
}
|
}
|
||||||
window.textsecure.messaging.sendRequestKeySyncMessage();
|
|
||||||
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
window.log.warn(
|
||||||
|
'stopStorageServiceSync: We are primary device; not sending key sync request'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessageSend(window.textsecure.messaging.sendRequestKeySyncMessage(), {
|
||||||
|
messageIds: [],
|
||||||
|
sendType: 'otherSync',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1106,7 +1129,18 @@ async function upload(fromSync = false): Promise<void> {
|
||||||
'storageService.upload: no storageKey, requesting new keys'
|
'storageService.upload: no storageKey, requesting new keys'
|
||||||
);
|
);
|
||||||
backOff.reset();
|
backOff.reset();
|
||||||
await window.textsecure.messaging.sendRequestKeySyncMessage();
|
|
||||||
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
window.log.warn(
|
||||||
|
'upload: We are primary device; not sending key sync request'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleMessageSend(
|
||||||
|
window.textsecure.messaging.sendRequestKeySyncMessage(),
|
||||||
|
{ messageIds: [], sendType: 'otherSync' }
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
// Copyright 2019-2021 Signal Messenger, LLC
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { handleMessageSend } from '../util/handleMessageSend';
|
||||||
|
import { getSendOptions } from '../util/getSendOptions';
|
||||||
|
|
||||||
export async function sendStickerPackSync(
|
export async function sendStickerPackSync(
|
||||||
packId: string,
|
packId: string,
|
||||||
packKey: string,
|
packKey: string,
|
||||||
installed: boolean
|
installed: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { ConversationController, textsecure, log } = window;
|
const { ConversationController, textsecure, log } = window;
|
||||||
const ourNumber = textsecure.storage.user.getNumber();
|
const ourConversation = ConversationController.getOurConversationOrThrow();
|
||||||
const { wrap, sendOptions } = await ConversationController.prepareForSend(
|
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||||
ourNumber,
|
syncMessage: true,
|
||||||
{
|
});
|
||||||
syncMessage: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!textsecure.messaging) {
|
if (!textsecure.messaging) {
|
||||||
log.error(
|
log.error(
|
||||||
|
@ -23,7 +23,14 @@ export async function sendStickerPackSync(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
wrap(
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
window.log.warn(
|
||||||
|
'shims/sendStickerPackSync: We are primary device; not sending sync'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessageSend(
|
||||||
textsecure.messaging.sendStickerPackSync(
|
textsecure.messaging.sendStickerPackSync(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
@ -33,7 +40,8 @@ export async function sendStickerPackSync(
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sendOptions
|
sendOptions
|
||||||
)
|
),
|
||||||
|
{ messageIds: [], sendType: 'otherSync' }
|
||||||
).catch(error => {
|
).catch(error => {
|
||||||
log.error(
|
log.error(
|
||||||
'shim: Error calling sendStickerPackSync:',
|
'shim: Error calling sendStickerPackSync:',
|
||||||
|
|
|
@ -14,7 +14,6 @@ import {
|
||||||
cloneDeep,
|
cloneDeep,
|
||||||
compact,
|
compact,
|
||||||
fromPairs,
|
fromPairs,
|
||||||
toPairs,
|
|
||||||
get,
|
get,
|
||||||
groupBy,
|
groupBy,
|
||||||
isFunction,
|
isFunction,
|
||||||
|
@ -22,6 +21,8 @@ import {
|
||||||
map,
|
map,
|
||||||
omit,
|
omit,
|
||||||
set,
|
set,
|
||||||
|
toPairs,
|
||||||
|
uniq,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
|
|
||||||
import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto';
|
import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto';
|
||||||
|
@ -41,8 +42,8 @@ import { StoredJob } from '../jobs/types';
|
||||||
import {
|
import {
|
||||||
AttachmentDownloadJobType,
|
AttachmentDownloadJobType,
|
||||||
ClientInterface,
|
ClientInterface,
|
||||||
ClientSearchResultMessageType,
|
|
||||||
ClientJobType,
|
ClientJobType,
|
||||||
|
ClientSearchResultMessageType,
|
||||||
ConversationType,
|
ConversationType,
|
||||||
IdentityKeyType,
|
IdentityKeyType,
|
||||||
ItemKeyType,
|
ItemKeyType,
|
||||||
|
@ -52,6 +53,12 @@ import {
|
||||||
PreKeyType,
|
PreKeyType,
|
||||||
SearchResultMessageType,
|
SearchResultMessageType,
|
||||||
SenderKeyType,
|
SenderKeyType,
|
||||||
|
SentMessageDBType,
|
||||||
|
SentMessagesType,
|
||||||
|
SentProtoType,
|
||||||
|
SentProtoWithMessageIdsType,
|
||||||
|
SentRecipientsDBType,
|
||||||
|
SentRecipientsType,
|
||||||
ServerInterface,
|
ServerInterface,
|
||||||
SessionType,
|
SessionType,
|
||||||
SignedPreKeyType,
|
SignedPreKeyType,
|
||||||
|
@ -143,6 +150,17 @@ const dataInterface: ClientInterface = {
|
||||||
getAllSenderKeys,
|
getAllSenderKeys,
|
||||||
removeSenderKeyById,
|
removeSenderKeyById,
|
||||||
|
|
||||||
|
insertSentProto,
|
||||||
|
deleteSentProtosOlderThan,
|
||||||
|
deleteSentProtoByMessageId,
|
||||||
|
insertProtoRecipients,
|
||||||
|
deleteSentProtoRecipient,
|
||||||
|
getSentProtoByRecipient,
|
||||||
|
removeAllSentProtos,
|
||||||
|
getAllSentProtos,
|
||||||
|
_getAllSentProtoRecipients,
|
||||||
|
_getAllSentProtoMessageIds,
|
||||||
|
|
||||||
createOrUpdateSession,
|
createOrUpdateSession,
|
||||||
createOrUpdateSessions,
|
createOrUpdateSessions,
|
||||||
commitSessionsAndUnprocessed,
|
commitSessionsAndUnprocessed,
|
||||||
|
@ -771,6 +789,66 @@ async function removeSenderKeyById(id: string): Promise<void> {
|
||||||
return channels.removeSenderKeyById(id);
|
return channels.removeSenderKeyById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sent Protos
|
||||||
|
|
||||||
|
async function insertSentProto(
|
||||||
|
proto: SentProtoType,
|
||||||
|
options: {
|
||||||
|
messageIds: SentMessagesType;
|
||||||
|
recipients: SentRecipientsType;
|
||||||
|
}
|
||||||
|
): Promise<number> {
|
||||||
|
return channels.insertSentProto(proto, {
|
||||||
|
...options,
|
||||||
|
messageIds: uniq(options.messageIds),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function deleteSentProtosOlderThan(timestamp: number): Promise<void> {
|
||||||
|
await channels.deleteSentProtosOlderThan(timestamp);
|
||||||
|
}
|
||||||
|
async function deleteSentProtoByMessageId(messageId: string): Promise<void> {
|
||||||
|
await channels.deleteSentProtoByMessageId(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertProtoRecipients(options: {
|
||||||
|
id: number;
|
||||||
|
recipientUuid: string;
|
||||||
|
deviceIds: Array<number>;
|
||||||
|
}): Promise<void> {
|
||||||
|
await channels.insertProtoRecipients(options);
|
||||||
|
}
|
||||||
|
async function deleteSentProtoRecipient(options: {
|
||||||
|
timestamp: number;
|
||||||
|
recipientUuid: string;
|
||||||
|
deviceId: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
await channels.deleteSentProtoRecipient(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSentProtoByRecipient(options: {
|
||||||
|
now: number;
|
||||||
|
recipientUuid: string;
|
||||||
|
timestamp: number;
|
||||||
|
}): Promise<SentProtoWithMessageIdsType | undefined> {
|
||||||
|
return channels.getSentProtoByRecipient(options);
|
||||||
|
}
|
||||||
|
async function removeAllSentProtos(): Promise<void> {
|
||||||
|
await channels.removeAllSentProtos();
|
||||||
|
}
|
||||||
|
async function getAllSentProtos(): Promise<Array<SentProtoType>> {
|
||||||
|
return channels.getAllSentProtos();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test-only:
|
||||||
|
async function _getAllSentProtoRecipients(): Promise<
|
||||||
|
Array<SentRecipientsDBType>
|
||||||
|
> {
|
||||||
|
return channels._getAllSentProtoRecipients();
|
||||||
|
}
|
||||||
|
async function _getAllSentProtoMessageIds(): Promise<Array<SentMessageDBType>> {
|
||||||
|
return channels._getAllSentProtoMessageIds();
|
||||||
|
}
|
||||||
|
|
||||||
// Sessions
|
// Sessions
|
||||||
|
|
||||||
async function createOrUpdateSession(data: SessionType) {
|
async function createOrUpdateSession(data: SessionType) {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import type { ReactionType } from '../types/Reactions';
|
||||||
import type { ConversationColorType, CustomColorType } from '../types/Colors';
|
import type { ConversationColorType, CustomColorType } from '../types/Colors';
|
||||||
import { StorageAccessType } from '../types/Storage.d';
|
import { StorageAccessType } from '../types/Storage.d';
|
||||||
import type { AttachmentType } from '../types/Attachment';
|
import type { AttachmentType } from '../types/Attachment';
|
||||||
|
import { BodyRangesType } from '../types/Util';
|
||||||
|
|
||||||
export type AttachmentDownloadJobTypeType =
|
export type AttachmentDownloadJobTypeType =
|
||||||
| 'long-message'
|
| 'long-message'
|
||||||
|
@ -83,9 +84,32 @@ export type SearchResultMessageType = {
|
||||||
};
|
};
|
||||||
export type ClientSearchResultMessageType = MessageType & {
|
export type ClientSearchResultMessageType = MessageType & {
|
||||||
json: string;
|
json: string;
|
||||||
bodyRanges: [];
|
bodyRanges: BodyRangesType;
|
||||||
snippet: string;
|
snippet: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SentProtoType = {
|
||||||
|
contentHint: number;
|
||||||
|
proto: Buffer;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
export type SentProtoWithMessageIdsType = SentProtoType & {
|
||||||
|
messageIds: Array<string>;
|
||||||
|
};
|
||||||
|
export type SentRecipientsType = Record<string, Array<number>>;
|
||||||
|
export type SentMessagesType = Array<string>;
|
||||||
|
|
||||||
|
// These two are for test only
|
||||||
|
export type SentRecipientsDBType = {
|
||||||
|
payloadId: number;
|
||||||
|
recipientUuid: string;
|
||||||
|
deviceId: number;
|
||||||
|
};
|
||||||
|
export type SentMessageDBType = {
|
||||||
|
payloadId: number;
|
||||||
|
messageId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type SenderKeyType = {
|
export type SenderKeyType = {
|
||||||
// Primary key
|
// Primary key
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -215,6 +239,36 @@ export type DataInterface = {
|
||||||
getAllSenderKeys: () => Promise<Array<SenderKeyType>>;
|
getAllSenderKeys: () => Promise<Array<SenderKeyType>>;
|
||||||
removeSenderKeyById: (id: string) => Promise<void>;
|
removeSenderKeyById: (id: string) => Promise<void>;
|
||||||
|
|
||||||
|
insertSentProto: (
|
||||||
|
proto: SentProtoType,
|
||||||
|
options: {
|
||||||
|
recipients: SentRecipientsType;
|
||||||
|
messageIds: SentMessagesType;
|
||||||
|
}
|
||||||
|
) => Promise<number>;
|
||||||
|
deleteSentProtosOlderThan: (timestamp: number) => Promise<void>;
|
||||||
|
deleteSentProtoByMessageId: (messageId: string) => Promise<void>;
|
||||||
|
insertProtoRecipients: (options: {
|
||||||
|
id: number;
|
||||||
|
recipientUuid: string;
|
||||||
|
deviceIds: Array<number>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
deleteSentProtoRecipient: (options: {
|
||||||
|
timestamp: number;
|
||||||
|
recipientUuid: string;
|
||||||
|
deviceId: number;
|
||||||
|
}) => Promise<void>;
|
||||||
|
getSentProtoByRecipient: (options: {
|
||||||
|
now: number;
|
||||||
|
recipientUuid: string;
|
||||||
|
timestamp: number;
|
||||||
|
}) => Promise<SentProtoWithMessageIdsType | undefined>;
|
||||||
|
removeAllSentProtos: () => Promise<void>;
|
||||||
|
getAllSentProtos: () => Promise<Array<SentProtoType>>;
|
||||||
|
// Test-only
|
||||||
|
_getAllSentProtoRecipients: () => Promise<Array<SentRecipientsDBType>>;
|
||||||
|
_getAllSentProtoMessageIds: () => Promise<Array<SentMessageDBType>>;
|
||||||
|
|
||||||
createOrUpdateSession: (data: SessionType) => Promise<void>;
|
createOrUpdateSession: (data: SessionType) => Promise<void>;
|
||||||
createOrUpdateSessions: (array: Array<SessionType>) => Promise<void>;
|
createOrUpdateSessions: (array: Array<SessionType>) => Promise<void>;
|
||||||
commitSessionsAndUnprocessed(options: {
|
commitSessionsAndUnprocessed(options: {
|
||||||
|
@ -255,6 +309,36 @@ export type DataInterface = {
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
getNextTapToViewMessageTimestampToAgeOut: () => Promise<undefined | number>;
|
getNextTapToViewMessageTimestampToAgeOut: () => Promise<undefined | number>;
|
||||||
|
|
||||||
|
getUnreadCountForConversation: (conversationId: string) => Promise<number>;
|
||||||
|
getUnreadByConversationAndMarkRead: (
|
||||||
|
conversationId: string,
|
||||||
|
newestUnreadId: number,
|
||||||
|
readAt?: number
|
||||||
|
) => Promise<
|
||||||
|
Array<
|
||||||
|
Pick<MessageType, 'id' | 'source' | 'sourceUuid' | 'sent_at' | 'type'>
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
getUnreadReactionsAndMarkRead: (
|
||||||
|
conversationId: string,
|
||||||
|
newestUnreadId: number
|
||||||
|
) => Promise<
|
||||||
|
Array<
|
||||||
|
Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp' | 'messageId'>
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
markReactionAsRead: (
|
||||||
|
targetAuthorUuid: string,
|
||||||
|
targetTimestamp: number
|
||||||
|
) => Promise<ReactionType | undefined>;
|
||||||
|
removeReactionFromConversation: (reaction: {
|
||||||
|
emoji: string;
|
||||||
|
fromId: string;
|
||||||
|
targetAuthorUuid: string;
|
||||||
|
targetTimestamp: number;
|
||||||
|
}) => Promise<void>;
|
||||||
|
addReaction: (reactionObj: ReactionType) => Promise<void>;
|
||||||
|
|
||||||
getUnprocessedCount: () => Promise<number>;
|
getUnprocessedCount: () => Promise<number>;
|
||||||
getAllUnprocessed: () => Promise<Array<UnprocessedType>>;
|
getAllUnprocessed: () => Promise<Array<UnprocessedType>>;
|
||||||
updateUnprocessedAttempts: (id: string, attempts: number) => Promise<void>;
|
updateUnprocessedAttempts: (id: string, attempts: number) => Promise<void>;
|
||||||
|
@ -391,33 +475,6 @@ export type ServerInterface = DataInterface & {
|
||||||
ourConversationId: string;
|
ourConversationId: string;
|
||||||
}) => Promise<MessageType | undefined>;
|
}) => Promise<MessageType | undefined>;
|
||||||
getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>;
|
getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>;
|
||||||
getUnreadCountForConversation: (conversationId: string) => Promise<number>;
|
|
||||||
getUnreadByConversationAndMarkRead: (
|
|
||||||
conversationId: string,
|
|
||||||
newestUnreadId: number,
|
|
||||||
readAt?: number
|
|
||||||
) => Promise<
|
|
||||||
Array<
|
|
||||||
Pick<MessageType, 'id' | 'source' | 'sourceUuid' | 'sent_at' | 'type'>
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
getUnreadReactionsAndMarkRead: (
|
|
||||||
conversationId: string,
|
|
||||||
newestUnreadId: number
|
|
||||||
) => Promise<
|
|
||||||
Array<Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp'>>
|
|
||||||
>;
|
|
||||||
markReactionAsRead: (
|
|
||||||
targetAuthorUuid: string,
|
|
||||||
targetTimestamp: number
|
|
||||||
) => Promise<ReactionType | undefined>;
|
|
||||||
removeReactionFromConversation: (reaction: {
|
|
||||||
emoji: string;
|
|
||||||
fromId: string;
|
|
||||||
targetAuthorUuid: string;
|
|
||||||
targetTimestamp: number;
|
|
||||||
}) => Promise<void>;
|
|
||||||
addReaction: (reactionObj: ReactionType) => Promise<void>;
|
|
||||||
removeConversation: (id: Array<string> | string) => Promise<void>;
|
removeConversation: (id: Array<string> | string) => Promise<void>;
|
||||||
removeMessage: (id: string) => Promise<void>;
|
removeMessage: (id: string) => Promise<void>;
|
||||||
removeMessages: (ids: Array<string>) => Promise<void>;
|
removeMessages: (ids: Array<string>) => Promise<void>;
|
||||||
|
@ -530,33 +587,6 @@ export type ClientInterface = DataInterface & {
|
||||||
getTapToViewMessagesNeedingErase: (options: {
|
getTapToViewMessagesNeedingErase: (options: {
|
||||||
MessageCollection: typeof MessageModelCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}) => Promise<MessageModelCollectionType>;
|
}) => Promise<MessageModelCollectionType>;
|
||||||
getUnreadCountForConversation: (conversationId: string) => Promise<number>;
|
|
||||||
getUnreadByConversationAndMarkRead: (
|
|
||||||
conversationId: string,
|
|
||||||
newestUnreadId: number,
|
|
||||||
readAt?: number
|
|
||||||
) => Promise<
|
|
||||||
Array<
|
|
||||||
Pick<MessageType, 'id' | 'source' | 'sourceUuid' | 'sent_at' | 'type'>
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
getUnreadReactionsAndMarkRead: (
|
|
||||||
conversationId: string,
|
|
||||||
newestUnreadId: number
|
|
||||||
) => Promise<
|
|
||||||
Array<Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp'>>
|
|
||||||
>;
|
|
||||||
markReactionAsRead: (
|
|
||||||
targetAuthorUuid: string,
|
|
||||||
targetTimestamp: number
|
|
||||||
) => Promise<ReactionType | undefined>;
|
|
||||||
removeReactionFromConversation: (reaction: {
|
|
||||||
emoji: string;
|
|
||||||
fromId: string;
|
|
||||||
targetAuthorUuid: string;
|
|
||||||
targetTimestamp: number;
|
|
||||||
}) => Promise<void>;
|
|
||||||
addReaction: (reactionObj: ReactionType) => Promise<void>;
|
|
||||||
removeConversation: (
|
removeConversation: (
|
||||||
id: string,
|
id: string,
|
||||||
options: { Conversation: typeof ConversationModel }
|
options: { Conversation: typeof ConversationModel }
|
||||||
|
|
446
ts/sql/Server.ts
446
ts/sql/Server.ts
|
@ -36,23 +36,30 @@ import { combineNames } from '../util/combineNames';
|
||||||
import { dropNull } from '../util/dropNull';
|
import { dropNull } from '../util/dropNull';
|
||||||
import { isNormalNumber } from '../util/isNormalNumber';
|
import { isNormalNumber } from '../util/isNormalNumber';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
import { isNotNil } from '../util/isNotNil';
|
||||||
|
import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
||||||
import { ConversationColorType, CustomColorType } from '../types/Colors';
|
import { ConversationColorType, CustomColorType } from '../types/Colors';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AllItemsType,
|
||||||
AttachmentDownloadJobType,
|
AttachmentDownloadJobType,
|
||||||
ConversationMetricsType,
|
ConversationMetricsType,
|
||||||
ConversationType,
|
ConversationType,
|
||||||
EmojiType,
|
EmojiType,
|
||||||
IdentityKeyType,
|
IdentityKeyType,
|
||||||
AllItemsType,
|
|
||||||
ItemKeyType,
|
ItemKeyType,
|
||||||
ItemType,
|
ItemType,
|
||||||
|
MessageMetricsType,
|
||||||
MessageType,
|
MessageType,
|
||||||
MessageTypeUnhydrated,
|
MessageTypeUnhydrated,
|
||||||
MessageMetricsType,
|
|
||||||
PreKeyType,
|
PreKeyType,
|
||||||
SearchResultMessageType,
|
SearchResultMessageType,
|
||||||
SenderKeyType,
|
SenderKeyType,
|
||||||
|
SentMessageDBType,
|
||||||
|
SentMessagesType,
|
||||||
|
SentProtoType,
|
||||||
|
SentProtoWithMessageIdsType,
|
||||||
|
SentRecipientsDBType,
|
||||||
|
SentRecipientsType,
|
||||||
ServerInterface,
|
ServerInterface,
|
||||||
SessionType,
|
SessionType,
|
||||||
SignedPreKeyType,
|
SignedPreKeyType,
|
||||||
|
@ -63,14 +70,6 @@ import {
|
||||||
UnprocessedUpdateType,
|
UnprocessedUpdateType,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
|
|
||||||
declare global {
|
|
||||||
// We want to extend `Function`'s properties, so we need to use an interface.
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
interface Function {
|
|
||||||
needsSerial?: boolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type JSONRows = Array<{ readonly json: string }>;
|
type JSONRows = Array<{ readonly json: string }>;
|
||||||
type ConversationRow = Readonly<{
|
type ConversationRow = Readonly<{
|
||||||
json: string;
|
json: string;
|
||||||
|
@ -137,6 +136,17 @@ const dataInterface: ServerInterface = {
|
||||||
getAllSenderKeys,
|
getAllSenderKeys,
|
||||||
removeSenderKeyById,
|
removeSenderKeyById,
|
||||||
|
|
||||||
|
insertSentProto,
|
||||||
|
deleteSentProtosOlderThan,
|
||||||
|
deleteSentProtoByMessageId,
|
||||||
|
insertProtoRecipients,
|
||||||
|
deleteSentProtoRecipient,
|
||||||
|
getSentProtoByRecipient,
|
||||||
|
removeAllSentProtos,
|
||||||
|
getAllSentProtos,
|
||||||
|
_getAllSentProtoRecipients,
|
||||||
|
_getAllSentProtoMessageIds,
|
||||||
|
|
||||||
createOrUpdateSession,
|
createOrUpdateSession,
|
||||||
createOrUpdateSessions,
|
createOrUpdateSessions,
|
||||||
commitSessionsAndUnprocessed,
|
commitSessionsAndUnprocessed,
|
||||||
|
@ -253,16 +263,16 @@ type DatabaseQueryCache = Map<string, Statement<Array<any>>>;
|
||||||
|
|
||||||
const statementCache = new WeakMap<Database, DatabaseQueryCache>();
|
const statementCache = new WeakMap<Database, DatabaseQueryCache>();
|
||||||
|
|
||||||
function prepare(db: Database, query: string): Statement<Query> {
|
function prepare<T>(db: Database, query: string): Statement<T> {
|
||||||
let dbCache = statementCache.get(db);
|
let dbCache = statementCache.get(db);
|
||||||
if (!dbCache) {
|
if (!dbCache) {
|
||||||
dbCache = new Map();
|
dbCache = new Map();
|
||||||
statementCache.set(db, dbCache);
|
statementCache.set(db, dbCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = dbCache.get(query);
|
let result = dbCache.get(query) as Statement<T>;
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = db.prepare(query);
|
result = db.prepare<T>(query);
|
||||||
dbCache.set(query, result);
|
dbCache.set(query, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1947,6 +1957,84 @@ function updateToSchemaVersion36(currentVersion: number, db: Database) {
|
||||||
console.log('updateToSchemaVersion36: success!');
|
console.log('updateToSchemaVersion36: success!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateToSchemaVersion37(currentVersion: number, db: Database) {
|
||||||
|
if (currentVersion >= 37) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
db.exec(`
|
||||||
|
-- Create send log primary table
|
||||||
|
|
||||||
|
CREATE TABLE sendLogPayloads(
|
||||||
|
id INTEGER PRIMARY KEY ASC,
|
||||||
|
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
contentHint INTEGER NOT NULL,
|
||||||
|
proto BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX sendLogPayloadsByTimestamp ON sendLogPayloads (timestamp);
|
||||||
|
|
||||||
|
-- Create send log recipients table with foreign key relationship to payloads
|
||||||
|
|
||||||
|
CREATE TABLE sendLogRecipients(
|
||||||
|
payloadId INTEGER NOT NULL,
|
||||||
|
|
||||||
|
recipientUuid STRING NOT NULL,
|
||||||
|
deviceId INTEGER NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (payloadId, recipientUuid, deviceId),
|
||||||
|
|
||||||
|
CONSTRAINT sendLogRecipientsForeignKey
|
||||||
|
FOREIGN KEY (payloadId)
|
||||||
|
REFERENCES sendLogPayloads(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX sendLogRecipientsByRecipient
|
||||||
|
ON sendLogRecipients (recipientUuid, deviceId);
|
||||||
|
|
||||||
|
-- Create send log messages table with foreign key relationship to payloads
|
||||||
|
|
||||||
|
CREATE TABLE sendLogMessageIds(
|
||||||
|
payloadId INTEGER NOT NULL,
|
||||||
|
|
||||||
|
messageId STRING NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (payloadId, messageId),
|
||||||
|
|
||||||
|
CONSTRAINT sendLogMessageIdsForeignKey
|
||||||
|
FOREIGN KEY (payloadId)
|
||||||
|
REFERENCES sendLogPayloads(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX sendLogMessageIdsByMessage
|
||||||
|
ON sendLogMessageIds (messageId);
|
||||||
|
|
||||||
|
-- Recreate messages table delete trigger with send log support
|
||||||
|
|
||||||
|
DROP TRIGGER messages_on_delete;
|
||||||
|
|
||||||
|
CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN
|
||||||
|
DELETE FROM messages_fts WHERE rowid = old.rowid;
|
||||||
|
DELETE FROM sendLogPayloads WHERE id IN (
|
||||||
|
SELECT payloadId FROM sendLogMessageIds
|
||||||
|
WHERE messageId = old.id
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
--- Add messageId column to reactions table to properly track proto associations
|
||||||
|
|
||||||
|
ALTER TABLE reactions ADD column messageId STRING;
|
||||||
|
`);
|
||||||
|
|
||||||
|
db.pragma('user_version = 37');
|
||||||
|
})();
|
||||||
|
console.log('updateToSchemaVersion37: success!');
|
||||||
|
}
|
||||||
|
|
||||||
const SCHEMA_VERSIONS = [
|
const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion1,
|
updateToSchemaVersion1,
|
||||||
updateToSchemaVersion2,
|
updateToSchemaVersion2,
|
||||||
|
@ -1984,6 +2072,7 @@ const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion34,
|
updateToSchemaVersion34,
|
||||||
updateToSchemaVersion35,
|
updateToSchemaVersion35,
|
||||||
updateToSchemaVersion36,
|
updateToSchemaVersion36,
|
||||||
|
updateToSchemaVersion37,
|
||||||
];
|
];
|
||||||
|
|
||||||
function updateSchema(db: Database): void {
|
function updateSchema(db: Database): void {
|
||||||
|
@ -2350,11 +2439,11 @@ async function getSenderKeyById(
|
||||||
}
|
}
|
||||||
async function removeAllSenderKeys(): Promise<void> {
|
async function removeAllSenderKeys(): Promise<void> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
prepare(db, 'DELETE FROM senderKeys').run({});
|
prepare<EmptyQuery>(db, 'DELETE FROM senderKeys').run();
|
||||||
}
|
}
|
||||||
async function getAllSenderKeys(): Promise<Array<SenderKeyType>> {
|
async function getAllSenderKeys(): Promise<Array<SenderKeyType>> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
const rows = prepare(db, 'SELECT * FROM senderKeys').all({});
|
const rows = prepare<EmptyQuery>(db, 'SELECT * FROM senderKeys').all();
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
@ -2363,6 +2452,317 @@ async function removeSenderKeyById(id: string): Promise<void> {
|
||||||
prepare(db, 'DELETE FROM senderKeys WHERE id = $id').run({ id });
|
prepare(db, 'DELETE FROM senderKeys WHERE id = $id').run({ id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function insertSentProto(
|
||||||
|
proto: SentProtoType,
|
||||||
|
options: {
|
||||||
|
recipients: SentRecipientsType;
|
||||||
|
messageIds: SentMessagesType;
|
||||||
|
}
|
||||||
|
): Promise<number> {
|
||||||
|
const db = getInstance();
|
||||||
|
const { recipients, messageIds } = options;
|
||||||
|
|
||||||
|
// Note: we use `pluck` in this function to fetch only the first column of returned row.
|
||||||
|
|
||||||
|
return db.transaction(() => {
|
||||||
|
// 1. Insert the payload, fetching its primary key id
|
||||||
|
const info = prepare(
|
||||||
|
db,
|
||||||
|
`
|
||||||
|
INSERT INTO sendLogPayloads (
|
||||||
|
contentHint,
|
||||||
|
proto,
|
||||||
|
timestamp
|
||||||
|
) VALUES (
|
||||||
|
$contentHint,
|
||||||
|
$proto,
|
||||||
|
$timestamp
|
||||||
|
);
|
||||||
|
`
|
||||||
|
).run(proto);
|
||||||
|
const id = parseIntOrThrow(
|
||||||
|
info.lastInsertRowid,
|
||||||
|
'insertSentProto/lastInsertRowid'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Insert a record for each recipient device.
|
||||||
|
const recipientStatement = prepare(
|
||||||
|
db,
|
||||||
|
`
|
||||||
|
INSERT INTO sendLogRecipients (
|
||||||
|
payloadId,
|
||||||
|
recipientUuid,
|
||||||
|
deviceId
|
||||||
|
) VALUES (
|
||||||
|
$id,
|
||||||
|
$recipientUuid,
|
||||||
|
$deviceId
|
||||||
|
);
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const recipientUuids = Object.keys(recipients);
|
||||||
|
for (const recipientUuid of recipientUuids) {
|
||||||
|
const deviceIds = recipients[recipientUuid];
|
||||||
|
|
||||||
|
for (const deviceId of deviceIds) {
|
||||||
|
recipientStatement.run({
|
||||||
|
id,
|
||||||
|
recipientUuid,
|
||||||
|
deviceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Insert a record for each message referenced by this payload.
|
||||||
|
const messageStatement = prepare(
|
||||||
|
db,
|
||||||
|
`
|
||||||
|
INSERT INTO sendLogMessageIds (
|
||||||
|
payloadId,
|
||||||
|
messageId
|
||||||
|
) VALUES (
|
||||||
|
$id,
|
||||||
|
$messageId
|
||||||
|
);
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const messageId of messageIds) {
|
||||||
|
messageStatement.run({
|
||||||
|
id,
|
||||||
|
messageId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSentProtosOlderThan(timestamp: number): Promise<void> {
|
||||||
|
const db = getInstance();
|
||||||
|
|
||||||
|
prepare(
|
||||||
|
db,
|
||||||
|
`
|
||||||
|
DELETE FROM sendLogPayloads
|
||||||
|
WHERE
|
||||||
|
timestamp IS NULL OR
|
||||||
|
timestamp < $timestamp;
|
||||||
|
`
|
||||||
|
).run({
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSentProtoByMessageId(messageId: string): Promise<void> {
|
||||||
|
const db = getInstance();
|
||||||
|
|
||||||
|
prepare(
|
||||||
|
db,
|
||||||
|
`
|
||||||
|
DELETE FROM sendLogPayloads WHERE id IN (
|
||||||
|
SELECT payloadId FROM sendLogMessageIds
|
||||||
|
WHERE messageId = $messageId
|
||||||
|
);
|
||||||
|
`
|
||||||
|
).run({
|
||||||
|
messageId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertProtoRecipients({
|
||||||
|
id,
|
||||||
|
recipientUuid,
|
||||||
|
deviceIds,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
recipientUuid: string;
|
||||||
|
deviceIds: Array<number>;
|
||||||
|
}): Promise<void> {
|
||||||
|
const db = getInstance();
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
const statement = prepare(
|
||||||
|
db,
|
||||||
|
`
|
||||||
|
INSERT INTO sendLogRecipients (
|
||||||
|
payloadId,
|
||||||
|
recipientUuid,
|
||||||
|
deviceId
|
||||||
|
) VALUES (
|
||||||
|
$id,
|
||||||
|
$recipientUuid,
|
||||||
|
$deviceId
|
||||||
|
);
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const deviceId of deviceIds) {
|
||||||
|
statement.run({
|
||||||
|
id,
|
||||||
|
recipientUuid,
|
||||||
|
deviceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSentProtoRecipient({
|
||||||
|
timestamp,
|
||||||
|
recipientUuid,
|
||||||
|
deviceId,
|
||||||
|
}: {
|
||||||
|
timestamp: number;
|
||||||
|
recipientUuid: string;
|
||||||
|
deviceId: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
const db = getInstance();
|
||||||
|
|
||||||
|
// Note: we use `pluck` in this function to fetch only the first column of returned row.
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
// 1. Figure out what payload we're talking about.
|
||||||
|
const rows = prepare(
|
||||||
|
db,
|
||||||
|
`
|
||||||
|
SELECT sendLogPayloads.id FROM sendLogPayloads
|
||||||
|
INNER JOIN sendLogRecipients
|
||||||
|
ON sendLogRecipients.payloadId = sendLogPayloads.id
|
||||||
|
WHERE
|
||||||
|
sendLogPayloads.timestamp = $timestamp AND
|
||||||
|
sendLogRecipients.recipientUuid = $recipientUuid AND
|
||||||
|
sendLogRecipients.deviceId = $deviceId;
|
||||||
|
`
|
||||||
|
).all({ timestamp, recipientUuid, deviceId });
|
||||||
|
if (!rows.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rows.length > 1) {
|
||||||
|
console.warn(
|
||||||
|
`deleteSentProtoRecipient: More than one payload matches recipient and timestamp ${timestamp}. Using the first.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = rows[0];
|
||||||
|
|
||||||
|
// 2. Delete the recipient/device combination in question.
|
||||||
|
prepare(
|
||||||
|
db,
|
||||||
|
`
|
||||||
|
DELETE FROM sendLogRecipients
|
||||||
|
WHERE
|
||||||
|
payloadId = $id AND
|
||||||
|
recipientUuid = $recipientUuid AND
|
||||||
|
deviceId = $deviceId;
|
||||||
|
`
|
||||||
|
).run({ id, recipientUuid, deviceId });
|
||||||
|
|
||||||
|
// 3. See how many more recipient devices there were for this payload.
|
||||||
|
const remaining = prepare(
|
||||||
|
db,
|
||||||
|
'SELECT count(*) FROM sendLogRecipients WHERE payloadId = $id;'
|
||||||
|
)
|
||||||
|
.pluck(true)
|
||||||
|
.get({ id });
|
||||||
|
|
||||||
|
if (!isNumber(remaining)) {
|
||||||
|
throw new Error(
|
||||||
|
'deleteSentProtoRecipient: select count() returned non-number!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Delete the entire payload if there are no more recipients left.
|
||||||
|
console.info(
|
||||||
|
`deleteSentProtoRecipient: Deleting proto payload for timestamp ${timestamp}`
|
||||||
|
);
|
||||||
|
prepare(db, 'DELETE FROM sendLogPayloads WHERE id = $id;').run({
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSentProtoByRecipient({
|
||||||
|
now,
|
||||||
|
recipientUuid,
|
||||||
|
timestamp,
|
||||||
|
}: {
|
||||||
|
now: number;
|
||||||
|
recipientUuid: string;
|
||||||
|
timestamp: number;
|
||||||
|
}): Promise<SentProtoWithMessageIdsType | undefined> {
|
||||||
|
const db = getInstance();
|
||||||
|
|
||||||
|
const HOUR = 1000 * 60 * 60;
|
||||||
|
const oneDayAgo = now - HOUR * 24;
|
||||||
|
|
||||||
|
await deleteSentProtosOlderThan(oneDayAgo);
|
||||||
|
|
||||||
|
const row = prepare(
|
||||||
|
db,
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
sendLogPayloads.*,
|
||||||
|
GROUP_CONCAT(DISTINCT sendLogMessageIds.messageId) AS messageIds
|
||||||
|
FROM sendLogPayloads
|
||||||
|
INNER JOIN sendLogRecipients ON sendLogRecipients.payloadId = sendLogPayloads.id
|
||||||
|
LEFT JOIN sendLogMessageIds ON sendLogMessageIds.payloadId = sendLogPayloads.id
|
||||||
|
WHERE
|
||||||
|
sendLogPayloads.timestamp = $timestamp AND
|
||||||
|
sendLogRecipients.recipientUuid = $recipientUuid
|
||||||
|
GROUP BY sendLogPayloads.id;
|
||||||
|
`
|
||||||
|
).get({
|
||||||
|
timestamp,
|
||||||
|
recipientUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messageIds } = row;
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
messageIds: messageIds ? messageIds.split(',') : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function removeAllSentProtos(): Promise<void> {
|
||||||
|
const db = getInstance();
|
||||||
|
prepare<EmptyQuery>(db, 'DELETE FROM sendLogPayloads;').run();
|
||||||
|
}
|
||||||
|
async function getAllSentProtos(): Promise<Array<SentProtoType>> {
|
||||||
|
const db = getInstance();
|
||||||
|
const rows = prepare<EmptyQuery>(db, 'SELECT * FROM sendLogPayloads;').all();
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
async function _getAllSentProtoRecipients(): Promise<
|
||||||
|
Array<SentRecipientsDBType>
|
||||||
|
> {
|
||||||
|
const db = getInstance();
|
||||||
|
const rows = prepare<EmptyQuery>(
|
||||||
|
db,
|
||||||
|
'SELECT * FROM sendLogRecipients;'
|
||||||
|
).all();
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
async function _getAllSentProtoMessageIds(): Promise<Array<SentMessageDBType>> {
|
||||||
|
const db = getInstance();
|
||||||
|
const rows = prepare<EmptyQuery>(
|
||||||
|
db,
|
||||||
|
'SELECT * FROM sendLogMessageIds;'
|
||||||
|
).all();
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
const SESSIONS_TABLE = 'sessions';
|
const SESSIONS_TABLE = 'sessions';
|
||||||
function createOrUpdateSessionSync(data: SessionType): void {
|
function createOrUpdateSessionSync(data: SessionType): void {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
|
@ -2717,8 +3117,7 @@ function updateConversationSync(data: ConversationType): void {
|
||||||
? members.join(' ')
|
? members.join(' ')
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
prepare(
|
db.prepare(
|
||||||
db,
|
|
||||||
`
|
`
|
||||||
UPDATE conversations SET
|
UPDATE conversations SET
|
||||||
json = $json,
|
json = $json,
|
||||||
|
@ -3470,13 +3869,18 @@ async function getUnreadByConversationAndMarkRead(
|
||||||
async function getUnreadReactionsAndMarkRead(
|
async function getUnreadReactionsAndMarkRead(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
newestUnreadId: number
|
newestUnreadId: number
|
||||||
): Promise<Array<Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp'>>> {
|
): Promise<
|
||||||
|
Array<
|
||||||
|
Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp' | 'messageId'>
|
||||||
|
>
|
||||||
|
> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
|
|
||||||
return db.transaction(() => {
|
return db.transaction(() => {
|
||||||
const unreadMessages = db
|
const unreadMessages = db
|
||||||
.prepare<Query>(
|
.prepare<Query>(
|
||||||
`
|
`
|
||||||
SELECT targetAuthorUuid, targetTimestamp
|
SELECT targetAuthorUuid, targetTimestamp, messageId
|
||||||
FROM reactions WHERE
|
FROM reactions WHERE
|
||||||
unread = 1 AND
|
unread = 1 AND
|
||||||
conversationId = $conversationId AND
|
conversationId = $conversationId AND
|
||||||
|
@ -3548,6 +3952,7 @@ async function addReaction({
|
||||||
conversationId,
|
conversationId,
|
||||||
emoji,
|
emoji,
|
||||||
fromId,
|
fromId,
|
||||||
|
messageId,
|
||||||
messageReceivedAt,
|
messageReceivedAt,
|
||||||
targetAuthorUuid,
|
targetAuthorUuid,
|
||||||
targetTimestamp,
|
targetTimestamp,
|
||||||
|
@ -3559,6 +3964,7 @@ async function addReaction({
|
||||||
conversationId,
|
conversationId,
|
||||||
emoji,
|
emoji,
|
||||||
fromId,
|
fromId,
|
||||||
|
messageId,
|
||||||
messageReceivedAt,
|
messageReceivedAt,
|
||||||
targetAuthorUuid,
|
targetAuthorUuid,
|
||||||
targetTimestamp,
|
targetTimestamp,
|
||||||
|
@ -3567,6 +3973,7 @@ async function addReaction({
|
||||||
$conversationId,
|
$conversationId,
|
||||||
$emoji,
|
$emoji,
|
||||||
$fromId,
|
$fromId,
|
||||||
|
$messageId,
|
||||||
$messageReceivedAt,
|
$messageReceivedAt,
|
||||||
$targetAuthorUuid,
|
$targetAuthorUuid,
|
||||||
$targetTimestamp,
|
$targetTimestamp,
|
||||||
|
@ -3577,6 +3984,7 @@ async function addReaction({
|
||||||
conversationId,
|
conversationId,
|
||||||
emoji,
|
emoji,
|
||||||
fromId,
|
fromId,
|
||||||
|
messageId,
|
||||||
messageReceivedAt,
|
messageReceivedAt,
|
||||||
targetAuthorUuid,
|
targetAuthorUuid,
|
||||||
targetTimestamp,
|
targetTimestamp,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { assert } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
import { CallbackResultType } from '../../textsecure/SendMessage';
|
||||||
import { SignalService as Proto } from '../../protobuf';
|
import { SignalService as Proto } from '../../protobuf';
|
||||||
|
|
||||||
describe('Message', () => {
|
describe('Message', () => {
|
||||||
|
@ -71,7 +72,16 @@ describe('Message', () => {
|
||||||
it('updates the `sent` attribute', async () => {
|
it('updates the `sent` attribute', async () => {
|
||||||
const message = createMessage({ type: 'outgoing', source, sent: false });
|
const message = createMessage({ type: 'outgoing', source, sent: false });
|
||||||
|
|
||||||
await message.send(Promise.resolve({}));
|
const promise: Promise<CallbackResultType> = Promise.resolve({
|
||||||
|
successfulIdentifiers: [window.getGuid(), window.getGuid()],
|
||||||
|
errors: [
|
||||||
|
Object.assign(new Error('failed'), {
|
||||||
|
identifier: window.getGuid(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await message.send(promise);
|
||||||
|
|
||||||
assert.isTrue(message.get('sent'));
|
assert.isTrue(message.get('sent'));
|
||||||
});
|
});
|
||||||
|
|
591
ts/test-electron/sql/sendLog_test.ts
Normal file
591
ts/test-electron/sql/sendLog_test.ts
Normal file
|
@ -0,0 +1,591 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { v4 as getGuid } from 'uuid';
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import dataInterface from '../../sql/Client';
|
||||||
|
import {
|
||||||
|
constantTimeEqual,
|
||||||
|
getRandomBytes,
|
||||||
|
typedArrayToArrayBuffer,
|
||||||
|
} from '../../Crypto';
|
||||||
|
|
||||||
|
const {
|
||||||
|
_getAllSentProtoMessageIds,
|
||||||
|
_getAllSentProtoRecipients,
|
||||||
|
deleteSentProtoByMessageId,
|
||||||
|
deleteSentProtoRecipient,
|
||||||
|
deleteSentProtosOlderThan,
|
||||||
|
getAllSentProtos,
|
||||||
|
getSentProtoByRecipient,
|
||||||
|
insertProtoRecipients,
|
||||||
|
insertSentProto,
|
||||||
|
removeAllSentProtos,
|
||||||
|
removeMessage,
|
||||||
|
saveMessage,
|
||||||
|
} = dataInterface;
|
||||||
|
|
||||||
|
describe('sendLog', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await removeAllSentProtos();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('roundtrips with insertSentProto/getAllSentProtos', async () => {
|
||||||
|
const bytes = Buffer.from(getRandomBytes(128));
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const proto = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: bytes,
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
await insertSentProto(proto, {
|
||||||
|
messageIds: [getGuid()],
|
||||||
|
recipients: {
|
||||||
|
[getGuid()]: [1, 2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const allProtos = await getAllSentProtos();
|
||||||
|
|
||||||
|
assert.lengthOf(allProtos, 1);
|
||||||
|
const actual = allProtos[0];
|
||||||
|
|
||||||
|
assert.strictEqual(actual.contentHint, proto.contentHint);
|
||||||
|
assert.isTrue(
|
||||||
|
constantTimeEqual(
|
||||||
|
typedArrayToArrayBuffer(actual.proto),
|
||||||
|
typedArrayToArrayBuffer(proto.proto)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.strictEqual(actual.timestamp, proto.timestamp);
|
||||||
|
|
||||||
|
await removeAllSentProtos();
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cascades deletes into both tables with foreign keys', async () => {
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 0);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 0);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 0);
|
||||||
|
|
||||||
|
const bytes = Buffer.from(getRandomBytes(128));
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const proto = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: bytes,
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
await insertSentProto(proto, {
|
||||||
|
messageIds: [getGuid(), getGuid()],
|
||||||
|
recipients: {
|
||||||
|
[getGuid()]: [1, 2],
|
||||||
|
[getGuid()]: [1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 2);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 3);
|
||||||
|
|
||||||
|
await removeAllSentProtos();
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 0);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 0);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trigger deletes payload when referenced message is deleted', async () => {
|
||||||
|
const id = getGuid();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
await saveMessage(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
|
||||||
|
body: 'some text',
|
||||||
|
conversationId: getGuid(),
|
||||||
|
received_at: timestamp,
|
||||||
|
sent_at: timestamp,
|
||||||
|
timestamp,
|
||||||
|
type: 'outgoing',
|
||||||
|
},
|
||||||
|
{ forceSave: true, Message: window.Whisper.Message }
|
||||||
|
);
|
||||||
|
|
||||||
|
const bytes = Buffer.from(getRandomBytes(128));
|
||||||
|
const proto = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: bytes,
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
await insertSentProto(proto, {
|
||||||
|
messageIds: [id],
|
||||||
|
recipients: {
|
||||||
|
[getGuid()]: [1, 2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const allProtos = await getAllSentProtos();
|
||||||
|
|
||||||
|
assert.lengthOf(allProtos, 1);
|
||||||
|
const actual = allProtos[0];
|
||||||
|
|
||||||
|
assert.strictEqual(actual.timestamp, proto.timestamp);
|
||||||
|
|
||||||
|
await removeMessage(id, { Message: window.Whisper.Message });
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#insertSentProto', () => {
|
||||||
|
it('supports adding duplicates', async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
const messageIds = [getGuid()];
|
||||||
|
const recipients = {
|
||||||
|
[getGuid()]: [1],
|
||||||
|
};
|
||||||
|
const proto1 = {
|
||||||
|
contentHint: 7,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
const proto2 = {
|
||||||
|
contentHint: 9,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 0);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 0);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 0);
|
||||||
|
|
||||||
|
await insertSentProto(proto1, { messageIds, recipients });
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 1);
|
||||||
|
|
||||||
|
await insertSentProto(proto2, { messageIds, recipients });
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 2);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 2);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#insertProtoRecipients', () => {
|
||||||
|
it('handles duplicates, adding new recipients if needed', async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
const messageIds = [getGuid()];
|
||||||
|
const proto = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 0);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 0);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 0);
|
||||||
|
|
||||||
|
const id = await insertSentProto(proto, {
|
||||||
|
messageIds,
|
||||||
|
recipients: {
|
||||||
|
[getGuid()]: [1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 1);
|
||||||
|
|
||||||
|
const recipientUuid = getGuid();
|
||||||
|
await insertProtoRecipients({
|
||||||
|
id,
|
||||||
|
recipientUuid,
|
||||||
|
deviceIds: [1, 2],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#deleteSentProtosOlderThan', () => {
|
||||||
|
it('deletes all older timestamps', async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
const proto1 = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp: timestamp + 10,
|
||||||
|
};
|
||||||
|
const proto2 = {
|
||||||
|
contentHint: 2,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
const proto3 = {
|
||||||
|
contentHint: 0,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp: timestamp - 15,
|
||||||
|
};
|
||||||
|
await insertSentProto(proto1, {
|
||||||
|
messageIds: [getGuid()],
|
||||||
|
recipients: {
|
||||||
|
[getGuid()]: [1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await insertSentProto(proto2, {
|
||||||
|
messageIds: [getGuid()],
|
||||||
|
recipients: {
|
||||||
|
[getGuid()]: [1, 2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await insertSentProto(proto3, {
|
||||||
|
messageIds: [getGuid()],
|
||||||
|
recipients: {
|
||||||
|
[getGuid()]: [1, 2, 3],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 3);
|
||||||
|
|
||||||
|
await deleteSentProtosOlderThan(timestamp);
|
||||||
|
|
||||||
|
const allProtos = await getAllSentProtos();
|
||||||
|
assert.lengthOf(allProtos, 2);
|
||||||
|
|
||||||
|
const actual1 = allProtos[0];
|
||||||
|
assert.strictEqual(actual1.contentHint, proto1.contentHint);
|
||||||
|
assert.isTrue(
|
||||||
|
constantTimeEqual(
|
||||||
|
typedArrayToArrayBuffer(actual1.proto),
|
||||||
|
typedArrayToArrayBuffer(proto1.proto)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.strictEqual(actual1.timestamp, proto1.timestamp);
|
||||||
|
|
||||||
|
const actual2 = allProtos[1];
|
||||||
|
assert.strictEqual(actual2.contentHint, proto2.contentHint);
|
||||||
|
assert.isTrue(
|
||||||
|
constantTimeEqual(
|
||||||
|
typedArrayToArrayBuffer(actual2.proto),
|
||||||
|
typedArrayToArrayBuffer(proto2.proto)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.strictEqual(actual2.timestamp, proto2.timestamp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#deleteSentProtoByMessageId', () => {
|
||||||
|
it('deletes all records releated to that messageId', async () => {
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 0);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 0);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 0);
|
||||||
|
|
||||||
|
const messageId = getGuid();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const proto1 = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
const proto2 = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp: timestamp - 10,
|
||||||
|
};
|
||||||
|
const proto3 = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp: timestamp - 20,
|
||||||
|
};
|
||||||
|
await insertSentProto(proto1, {
|
||||||
|
messageIds: [messageId, getGuid()],
|
||||||
|
recipients: {
|
||||||
|
[getGuid()]: [1, 2],
|
||||||
|
[getGuid()]: [1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await insertSentProto(proto2, {
|
||||||
|
messageIds: [messageId],
|
||||||
|
recipients: {
|
||||||
|
[getGuid()]: [1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await insertSentProto(proto3, {
|
||||||
|
messageIds: [getGuid()],
|
||||||
|
recipients: {
|
||||||
|
[getGuid()]: [1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 3);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 4);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 5);
|
||||||
|
|
||||||
|
await deleteSentProtoByMessageId(messageId);
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#deleteSentProtoRecipient', () => {
|
||||||
|
it('does not delete payload if recipient remains', async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
const recipientUuid1 = getGuid();
|
||||||
|
const recipientUuid2 = getGuid();
|
||||||
|
const proto = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
await insertSentProto(proto, {
|
||||||
|
messageIds: [getGuid()],
|
||||||
|
recipients: {
|
||||||
|
[recipientUuid1]: [1, 2],
|
||||||
|
[recipientUuid2]: [1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 3);
|
||||||
|
|
||||||
|
await deleteSentProtoRecipient({
|
||||||
|
timestamp,
|
||||||
|
recipientUuid: recipientUuid1,
|
||||||
|
deviceId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes payload if no recipients remain', async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
const recipientUuid1 = getGuid();
|
||||||
|
const recipientUuid2 = getGuid();
|
||||||
|
const proto = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
await insertSentProto(proto, {
|
||||||
|
messageIds: [getGuid()],
|
||||||
|
recipients: {
|
||||||
|
[recipientUuid1]: [1, 2],
|
||||||
|
[recipientUuid2]: [1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 3);
|
||||||
|
|
||||||
|
await deleteSentProtoRecipient({
|
||||||
|
timestamp,
|
||||||
|
recipientUuid: recipientUuid1,
|
||||||
|
deviceId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 2);
|
||||||
|
|
||||||
|
await deleteSentProtoRecipient({
|
||||||
|
timestamp,
|
||||||
|
recipientUuid: recipientUuid1,
|
||||||
|
deviceId: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 1);
|
||||||
|
|
||||||
|
await deleteSentProtoRecipient({
|
||||||
|
timestamp,
|
||||||
|
recipientUuid: recipientUuid2,
|
||||||
|
deviceId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 0);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#getSentProtoByRecipient', () => {
|
||||||
|
it('returns matching payload', async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
const recipientUuid = getGuid();
|
||||||
|
const messageIds = [getGuid(), getGuid()];
|
||||||
|
const proto = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
await insertSentProto(proto, {
|
||||||
|
messageIds,
|
||||||
|
recipients: {
|
||||||
|
[recipientUuid]: [1, 2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 2);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 2);
|
||||||
|
|
||||||
|
const actual = await getSentProtoByRecipient({
|
||||||
|
now: timestamp,
|
||||||
|
timestamp,
|
||||||
|
recipientUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!actual) {
|
||||||
|
throw new Error('Failed to fetch proto!');
|
||||||
|
}
|
||||||
|
assert.strictEqual(actual.contentHint, proto.contentHint);
|
||||||
|
assert.isTrue(
|
||||||
|
constantTimeEqual(
|
||||||
|
typedArrayToArrayBuffer(actual.proto),
|
||||||
|
typedArrayToArrayBuffer(proto.proto)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.strictEqual(actual.timestamp, proto.timestamp);
|
||||||
|
assert.sameMembers(actual.messageIds, messageIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns matching payload with no messageIds', async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
const recipientUuid = getGuid();
|
||||||
|
const proto = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
await insertSentProto(proto, {
|
||||||
|
messageIds: [],
|
||||||
|
recipients: {
|
||||||
|
[recipientUuid]: [1, 2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 2);
|
||||||
|
assert.lengthOf(await _getAllSentProtoMessageIds(), 0);
|
||||||
|
|
||||||
|
const actual = await getSentProtoByRecipient({
|
||||||
|
now: timestamp,
|
||||||
|
timestamp,
|
||||||
|
recipientUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!actual) {
|
||||||
|
throw new Error('Failed to fetch proto!');
|
||||||
|
}
|
||||||
|
assert.strictEqual(actual.contentHint, proto.contentHint);
|
||||||
|
assert.isTrue(
|
||||||
|
constantTimeEqual(
|
||||||
|
typedArrayToArrayBuffer(actual.proto),
|
||||||
|
typedArrayToArrayBuffer(proto.proto)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.strictEqual(actual.timestamp, proto.timestamp);
|
||||||
|
assert.deepEqual(actual.messageIds, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns nothing if payload does not have recipient', async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
const recipientUuid = getGuid();
|
||||||
|
const proto = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
await insertSentProto(proto, {
|
||||||
|
messageIds: [getGuid()],
|
||||||
|
recipients: {
|
||||||
|
[recipientUuid]: [1, 2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 2);
|
||||||
|
|
||||||
|
const actual = await getSentProtoByRecipient({
|
||||||
|
now: timestamp,
|
||||||
|
timestamp,
|
||||||
|
recipientUuid: getGuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.isUndefined(actual);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns nothing if timestamp does not match', async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
const recipientUuid = getGuid();
|
||||||
|
const proto = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
await insertSentProto(proto, {
|
||||||
|
messageIds: [getGuid()],
|
||||||
|
recipients: {
|
||||||
|
[recipientUuid]: [1, 2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 2);
|
||||||
|
|
||||||
|
const actual = await getSentProtoByRecipient({
|
||||||
|
now: timestamp,
|
||||||
|
timestamp: timestamp + 1,
|
||||||
|
recipientUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.isUndefined(actual);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns nothing if timestamp proto is too old', async () => {
|
||||||
|
const TWO_DAYS = 2 * 24 * 60 * 60 * 1000;
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
const recipientUuid = getGuid();
|
||||||
|
const proto = {
|
||||||
|
contentHint: 1,
|
||||||
|
proto: Buffer.from(getRandomBytes(128)),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
await insertSentProto(proto, {
|
||||||
|
messageIds: [getGuid()],
|
||||||
|
recipients: {
|
||||||
|
[recipientUuid]: [1, 2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 1);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 2);
|
||||||
|
|
||||||
|
const actual = await getSentProtoByRecipient({
|
||||||
|
now: timestamp + TWO_DAYS,
|
||||||
|
timestamp,
|
||||||
|
recipientUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.isUndefined(actual);
|
||||||
|
|
||||||
|
assert.lengthOf(await getAllSentProtos(), 0);
|
||||||
|
assert.lengthOf(await _getAllSentProtoRecipients(), 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1028,7 +1028,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const args = [
|
const args = [
|
||||||
'queueEncryptedEnvelope error handling envelope',
|
'queueEncryptedEnvelope error handling envelope',
|
||||||
this.getEnvelopeId(envelope),
|
this.getEnvelopeId(error.envelope || envelope),
|
||||||
':',
|
':',
|
||||||
error && error.extra ? JSON.stringify(error.extra) : '',
|
error && error.extra ? JSON.stringify(error.extra) : '',
|
||||||
error && error.stack ? error.stack : error,
|
error && error.stack ? error.stack : error,
|
||||||
|
@ -1587,7 +1587,10 @@ class MessageReceiverInner extends EventTarget {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Avoid deadlocks by scheduling processing on decrypted queue
|
// Avoid deadlocks by scheduling processing on decrypted queue
|
||||||
this.addToQueue(() => this.dispatchAndWait(event), TaskType.Decrypted);
|
this.addToQueue(
|
||||||
|
async () => this.dispatchEvent(event),
|
||||||
|
TaskType.Decrypted
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const envelopeId = this.getEnvelopeId(newEnvelope);
|
const envelopeId = this.getEnvelopeId(newEnvelope);
|
||||||
window.log.error(
|
window.log.error(
|
||||||
|
@ -1803,39 +1806,98 @@ class MessageReceiverInner extends EventTarget {
|
||||||
);
|
);
|
||||||
assert(envelope.content, 'Should have `content` field');
|
assert(envelope.content, 'Should have `content` field');
|
||||||
const result = await this.decrypt(stores, envelope, envelope.content);
|
const result = await this.decrypt(stores, envelope, envelope.content);
|
||||||
|
|
||||||
if (!result.plaintext) {
|
if (!result.plaintext) {
|
||||||
window.log.warn('decryptContentMessage: plaintext was falsey');
|
window.log.warn('decryptContentMessage: plaintext was falsey');
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
// Note: we need to process this as part of decryption, because we might need this
|
||||||
}
|
// sender key to decrypt the next message in the queue!
|
||||||
|
|
||||||
async innerHandleContentMessage(
|
|
||||||
envelope: ProcessedEnvelope,
|
|
||||||
plaintext: Uint8Array
|
|
||||||
): Promise<void> {
|
|
||||||
const content = Proto.Content.decode(plaintext);
|
|
||||||
|
|
||||||
// Note: a distribution message can be tacked on to any other message, so we
|
|
||||||
// make sure to process it first. If that fails, we still try to process
|
|
||||||
// the rest of the message.
|
|
||||||
try {
|
try {
|
||||||
|
const content = Proto.Content.decode(result.plaintext);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
content.senderKeyDistributionMessage &&
|
content.senderKeyDistributionMessage &&
|
||||||
Bytes.isNotEmpty(content.senderKeyDistributionMessage)
|
Bytes.isNotEmpty(content.senderKeyDistributionMessage)
|
||||||
) {
|
) {
|
||||||
await this.handleSenderKeyDistributionMessage(
|
await this.handleSenderKeyDistributionMessage(
|
||||||
envelope,
|
stores,
|
||||||
|
result.envelope,
|
||||||
content.senderKeyDistributionMessage
|
content.senderKeyDistributionMessage
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorString = error && error.stack ? error.stack : error;
|
const errorString = error && error.stack ? error.stack : error;
|
||||||
window.log.error(
|
window.log.error(
|
||||||
`innerHandleContentMessage: Failed to process sender key distribution message: ${errorString}`
|
`decryptContentMessage: Failed to process sender key distribution message: ${errorString}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async maybeUpdateTimestamp(
|
||||||
|
envelope: ProcessedEnvelope
|
||||||
|
): Promise<ProcessedEnvelope> {
|
||||||
|
const { retryPlaceholders } = window.Signal.Services;
|
||||||
|
if (!retryPlaceholders) {
|
||||||
|
window.log.warn(
|
||||||
|
'maybeUpdateTimestamp: retry placeholders not available!'
|
||||||
|
);
|
||||||
|
return envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { timestamp } = envelope;
|
||||||
|
const identifier =
|
||||||
|
envelope.groupId || envelope.sourceUuid || envelope.source;
|
||||||
|
const conversation = window.ConversationController.get(identifier);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!conversation) {
|
||||||
|
window.log.info(
|
||||||
|
`maybeUpdateTimestamp/${timestamp}: No conversation found for identifier ${identifier}`
|
||||||
|
);
|
||||||
|
return envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logId = `${conversation.idForLogging()}/${timestamp}`;
|
||||||
|
const item = await retryPlaceholders.findByMessageAndRemove(
|
||||||
|
conversation.id,
|
||||||
|
timestamp
|
||||||
|
);
|
||||||
|
if (item && item.wasOpened) {
|
||||||
|
window.log.info(
|
||||||
|
`maybeUpdateTimestamp/${logId}: found retry placeholder, but conversation was opened. No updates made.`
|
||||||
|
);
|
||||||
|
} else if (item) {
|
||||||
|
window.log.info(
|
||||||
|
`maybeUpdateTimestamp/${logId}: found retry placeholder. Updating receivedAtCounter/receivedAtDate`
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...envelope,
|
||||||
|
receivedAtCounter: item.receivedAtCounter,
|
||||||
|
receivedAtDate: item.receivedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorString = error && error.stack ? error.stack : error;
|
||||||
|
window.log.error(
|
||||||
|
`maybeUpdateTimestamp/${timestamp}: Failed to process sender key distribution message: ${errorString}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
async innerHandleContentMessage(
|
||||||
|
incomingEnvelope: ProcessedEnvelope,
|
||||||
|
plaintext: Uint8Array
|
||||||
|
): Promise<void> {
|
||||||
|
const content = Proto.Content.decode(plaintext);
|
||||||
|
const envelope = await this.maybeUpdateTimestamp(incomingEnvelope);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
content.decryptionErrorMessage &&
|
content.decryptionErrorMessage &&
|
||||||
Bytes.isNotEmpty(content.decryptionErrorMessage)
|
Bytes.isNotEmpty(content.decryptionErrorMessage)
|
||||||
|
@ -1908,10 +1970,11 @@ class MessageReceiverInner extends EventTarget {
|
||||||
senderDevice: request.deviceId(),
|
senderDevice: request.deviceId(),
|
||||||
sentAt: request.timestamp(),
|
sentAt: request.timestamp(),
|
||||||
});
|
});
|
||||||
await this.dispatchAndWait(event);
|
await this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSenderKeyDistributionMessage(
|
async handleSenderKeyDistributionMessage(
|
||||||
|
stores: LockedStores,
|
||||||
envelope: ProcessedEnvelope,
|
envelope: ProcessedEnvelope,
|
||||||
distributionMessage: Uint8Array
|
distributionMessage: Uint8Array
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -1941,12 +2004,15 @@ class MessageReceiverInner extends EventTarget {
|
||||||
const senderKeyStore = new SenderKeys();
|
const senderKeyStore = new SenderKeys();
|
||||||
const address = `${identifier}.${sourceDevice}`;
|
const address = `${identifier}.${sourceDevice}`;
|
||||||
|
|
||||||
await window.textsecure.storage.protocol.enqueueSenderKeyJob(address, () =>
|
await window.textsecure.storage.protocol.enqueueSenderKeyJob(
|
||||||
processSenderKeyDistributionMessage(
|
address,
|
||||||
sender,
|
() =>
|
||||||
senderKeyDistributionMessage,
|
processSenderKeyDistributionMessage(
|
||||||
senderKeyStore
|
sender,
|
||||||
)
|
senderKeyDistributionMessage,
|
||||||
|
senderKeyStore
|
||||||
|
),
|
||||||
|
stores.zone
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1989,6 +2055,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
envelopeTimestamp: envelope.timestamp,
|
envelopeTimestamp: envelope.timestamp,
|
||||||
source: envelope.source,
|
source: envelope.source,
|
||||||
sourceUuid: envelope.sourceUuid,
|
sourceUuid: envelope.sourceUuid,
|
||||||
|
sourceDevice: envelope.sourceDevice,
|
||||||
},
|
},
|
||||||
this.removeFromCache.bind(this, envelope)
|
this.removeFromCache.bind(this, envelope)
|
||||||
);
|
);
|
||||||
|
|
|
@ -48,6 +48,11 @@ export const enum SenderCertificateMode {
|
||||||
WithoutE164,
|
WithoutE164,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SendLogCallbackType = (options: {
|
||||||
|
identifier: string;
|
||||||
|
deviceIds: Array<number>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
type SendMetadata = {
|
type SendMetadata = {
|
||||||
type: number;
|
type: number;
|
||||||
destinationDeviceId: number;
|
destinationDeviceId: number;
|
||||||
|
@ -123,11 +128,11 @@ export default class OutgoingMessage {
|
||||||
|
|
||||||
errors: Array<CustomError>;
|
errors: Array<CustomError>;
|
||||||
|
|
||||||
successfulIdentifiers: Array<unknown>;
|
successfulIdentifiers: Array<string>;
|
||||||
|
|
||||||
failoverIdentifiers: Array<unknown>;
|
failoverIdentifiers: Array<string>;
|
||||||
|
|
||||||
unidentifiedDeliveries: Array<unknown>;
|
unidentifiedDeliveries: Array<string>;
|
||||||
|
|
||||||
sendMetadata?: SendMetadataType;
|
sendMetadata?: SendMetadataType;
|
||||||
|
|
||||||
|
@ -137,16 +142,31 @@ export default class OutgoingMessage {
|
||||||
|
|
||||||
contentHint: number;
|
contentHint: number;
|
||||||
|
|
||||||
constructor(
|
recipients: Record<string, Array<number>>;
|
||||||
server: WebAPIType,
|
|
||||||
timestamp: number,
|
sendLogCallback?: SendLogCallbackType;
|
||||||
identifiers: Array<string>,
|
|
||||||
message: Proto.Content | Proto.DataMessage | PlaintextContent,
|
constructor({
|
||||||
contentHint: number,
|
callback,
|
||||||
groupId: string | undefined,
|
contentHint,
|
||||||
callback: (result: CallbackResultType) => void,
|
groupId,
|
||||||
options: OutgoingMessageOptionsType = {}
|
identifiers,
|
||||||
) {
|
message,
|
||||||
|
options,
|
||||||
|
sendLogCallback,
|
||||||
|
server,
|
||||||
|
timestamp,
|
||||||
|
}: {
|
||||||
|
callback: (result: CallbackResultType) => void;
|
||||||
|
contentHint: number;
|
||||||
|
groupId: string | undefined;
|
||||||
|
identifiers: Array<string>;
|
||||||
|
message: Proto.Content | Proto.DataMessage | PlaintextContent;
|
||||||
|
options?: OutgoingMessageOptionsType;
|
||||||
|
sendLogCallback?: SendLogCallbackType;
|
||||||
|
server: WebAPIType;
|
||||||
|
timestamp: number;
|
||||||
|
}) {
|
||||||
if (message instanceof Proto.DataMessage) {
|
if (message instanceof Proto.DataMessage) {
|
||||||
const content = new Proto.Content();
|
const content = new Proto.Content();
|
||||||
content.dataMessage = message;
|
content.dataMessage = message;
|
||||||
|
@ -168,20 +188,29 @@ export default class OutgoingMessage {
|
||||||
this.successfulIdentifiers = [];
|
this.successfulIdentifiers = [];
|
||||||
this.failoverIdentifiers = [];
|
this.failoverIdentifiers = [];
|
||||||
this.unidentifiedDeliveries = [];
|
this.unidentifiedDeliveries = [];
|
||||||
|
this.recipients = {};
|
||||||
|
this.sendLogCallback = sendLogCallback;
|
||||||
|
|
||||||
const { sendMetadata, online } = options;
|
this.sendMetadata = options?.sendMetadata;
|
||||||
this.sendMetadata = sendMetadata;
|
this.online = options?.online;
|
||||||
this.online = online;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
numberCompleted(): void {
|
numberCompleted(): void {
|
||||||
this.identifiersCompleted += 1;
|
this.identifiersCompleted += 1;
|
||||||
if (this.identifiersCompleted >= this.identifiers.length) {
|
if (this.identifiersCompleted >= this.identifiers.length) {
|
||||||
|
const contentProto = this.getContentProtoBytes();
|
||||||
|
const { timestamp, contentHint, recipients } = this;
|
||||||
|
|
||||||
this.callback({
|
this.callback({
|
||||||
successfulIdentifiers: this.successfulIdentifiers,
|
successfulIdentifiers: this.successfulIdentifiers,
|
||||||
failoverIdentifiers: this.failoverIdentifiers,
|
failoverIdentifiers: this.failoverIdentifiers,
|
||||||
errors: this.errors,
|
errors: this.errors,
|
||||||
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
||||||
|
|
||||||
|
contentHint,
|
||||||
|
recipients,
|
||||||
|
contentProto,
|
||||||
|
timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -313,6 +342,14 @@ export default class OutgoingMessage {
|
||||||
return toArrayBuffer(this.plaintext);
|
return toArrayBuffer(this.plaintext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getContentProtoBytes(): Uint8Array | undefined {
|
||||||
|
if (this.message instanceof Proto.Content) {
|
||||||
|
return new Uint8Array(Proto.Content.encode(this.message).finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async getCiphertextMessage({
|
async getCiphertextMessage({
|
||||||
identityKeyStore,
|
identityKeyStore,
|
||||||
protocolAddress,
|
protocolAddress,
|
||||||
|
@ -455,9 +492,21 @@ export default class OutgoingMessage {
|
||||||
accessKey,
|
accessKey,
|
||||||
}).then(
|
}).then(
|
||||||
() => {
|
() => {
|
||||||
|
this.recipients[identifier] = deviceIds;
|
||||||
this.unidentifiedDeliveries.push(identifier);
|
this.unidentifiedDeliveries.push(identifier);
|
||||||
this.successfulIdentifiers.push(identifier);
|
this.successfulIdentifiers.push(identifier);
|
||||||
this.numberCompleted();
|
this.numberCompleted();
|
||||||
|
|
||||||
|
if (this.sendLogCallback) {
|
||||||
|
this.sendLogCallback({
|
||||||
|
identifier,
|
||||||
|
deviceIds,
|
||||||
|
});
|
||||||
|
} else if (this.successfulIdentifiers.length > 1) {
|
||||||
|
window.log.warn(
|
||||||
|
`OutgoingMessage.doSendMessage: no sendLogCallback provided for message ${this.timestamp}, but multiple recipients`
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async (error: Error) => {
|
async (error: Error) => {
|
||||||
if (error.code === 401 || error.code === 403) {
|
if (error.code === 401 || error.code === 403) {
|
||||||
|
@ -481,7 +530,19 @@ export default class OutgoingMessage {
|
||||||
return this.transmitMessage(identifier, jsonData, this.timestamp).then(
|
return this.transmitMessage(identifier, jsonData, this.timestamp).then(
|
||||||
() => {
|
() => {
|
||||||
this.successfulIdentifiers.push(identifier);
|
this.successfulIdentifiers.push(identifier);
|
||||||
|
this.recipients[identifier] = deviceIds;
|
||||||
this.numberCompleted();
|
this.numberCompleted();
|
||||||
|
|
||||||
|
if (this.sendLogCallback) {
|
||||||
|
this.sendLogCallback({
|
||||||
|
identifier,
|
||||||
|
deviceIds,
|
||||||
|
});
|
||||||
|
} else if (this.successfulIdentifiers.length > 1) {
|
||||||
|
window.log.warn(
|
||||||
|
`OutgoingMessage.doSendMessage: no sendLogCallback provided for message ${this.timestamp}, but multiple recipients`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
@ -28,7 +28,10 @@ import {
|
||||||
MultiRecipient200ResponseType,
|
MultiRecipient200ResponseType,
|
||||||
} from './WebAPI';
|
} from './WebAPI';
|
||||||
import createTaskWithTimeout from './TaskWithTimeout';
|
import createTaskWithTimeout from './TaskWithTimeout';
|
||||||
import OutgoingMessage, { SerializedCertificateType } from './OutgoingMessage';
|
import OutgoingMessage, {
|
||||||
|
SerializedCertificateType,
|
||||||
|
SendLogCallbackType,
|
||||||
|
} from './OutgoingMessage';
|
||||||
import Crypto from './Crypto';
|
import Crypto from './Crypto';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
import {
|
import {
|
||||||
|
@ -48,6 +51,11 @@ import {
|
||||||
LinkPreviewMetadata,
|
LinkPreviewMetadata,
|
||||||
} from '../linkPreviews/linkPreviewFetch';
|
} from '../linkPreviews/linkPreviewFetch';
|
||||||
import { concat } from '../util/iterables';
|
import { concat } from '../util/iterables';
|
||||||
|
import {
|
||||||
|
handleMessageSend,
|
||||||
|
shouldSaveProto,
|
||||||
|
SendTypesType,
|
||||||
|
} from '../util/handleMessageSend';
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
|
||||||
export type SendMetadataType = {
|
export type SendMetadataType = {
|
||||||
|
@ -68,11 +76,17 @@ export type CustomError = Error & {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CallbackResultType = {
|
export type CallbackResultType = {
|
||||||
successfulIdentifiers?: Array<any>;
|
successfulIdentifiers?: Array<string>;
|
||||||
failoverIdentifiers?: Array<any>;
|
failoverIdentifiers?: Array<string>;
|
||||||
errors?: Array<CustomError>;
|
errors?: Array<CustomError>;
|
||||||
unidentifiedDeliveries?: Array<any>;
|
unidentifiedDeliveries?: Array<string>;
|
||||||
dataMessage?: ArrayBuffer;
|
dataMessage?: ArrayBuffer;
|
||||||
|
|
||||||
|
// Fields necesary for send log save
|
||||||
|
contentHint?: number;
|
||||||
|
contentProto?: Uint8Array;
|
||||||
|
timestamp?: number;
|
||||||
|
recipients?: Record<string, Array<number>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PreviewType = {
|
type PreviewType = {
|
||||||
|
@ -593,9 +607,12 @@ export default class MessageSender {
|
||||||
try {
|
try {
|
||||||
const { sticker } = message;
|
const { sticker } = message;
|
||||||
|
|
||||||
if (!sticker || !sticker.data) {
|
if (!sticker) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!sticker.data) {
|
||||||
|
throw new Error('uploadSticker: No sticker data to upload!');
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
message.sticker = {
|
message.sticker = {
|
||||||
|
@ -824,21 +841,23 @@ export default class MessageSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessageProto({
|
sendMessageProto({
|
||||||
timestamp,
|
callback,
|
||||||
recipients,
|
|
||||||
proto,
|
|
||||||
contentHint,
|
contentHint,
|
||||||
groupId,
|
groupId,
|
||||||
callback,
|
|
||||||
options,
|
options,
|
||||||
|
proto,
|
||||||
|
recipients,
|
||||||
|
sendLogCallback,
|
||||||
|
timestamp,
|
||||||
}: {
|
}: {
|
||||||
timestamp: number;
|
callback: (result: CallbackResultType) => void;
|
||||||
recipients: Array<string>;
|
|
||||||
proto: Proto.Content | Proto.DataMessage | PlaintextContent;
|
|
||||||
contentHint: number;
|
contentHint: number;
|
||||||
groupId: string | undefined;
|
groupId: string | undefined;
|
||||||
callback: (result: CallbackResultType) => void;
|
|
||||||
options?: SendOptionsType;
|
options?: SendOptionsType;
|
||||||
|
proto: Proto.Content | Proto.DataMessage | PlaintextContent;
|
||||||
|
recipients: Array<string>;
|
||||||
|
sendLogCallback?: SendLogCallbackType;
|
||||||
|
timestamp: number;
|
||||||
}): void {
|
}): void {
|
||||||
const rejections = window.textsecure.storage.get(
|
const rejections = window.textsecure.storage.get(
|
||||||
'signedKeyRotationRejected',
|
'signedKeyRotationRejected',
|
||||||
|
@ -848,16 +867,17 @@ export default class MessageSender {
|
||||||
throw new SignedPreKeyRotationError();
|
throw new SignedPreKeyRotationError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const outgoing = new OutgoingMessage(
|
const outgoing = new OutgoingMessage({
|
||||||
this.server,
|
callback,
|
||||||
timestamp,
|
|
||||||
recipients,
|
|
||||||
proto,
|
|
||||||
contentHint,
|
contentHint,
|
||||||
groupId,
|
groupId,
|
||||||
callback,
|
identifiers: recipients,
|
||||||
options
|
message: proto,
|
||||||
);
|
options,
|
||||||
|
sendLogCallback,
|
||||||
|
server: this.server,
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
recipients.forEach(identifier => {
|
recipients.forEach(identifier => {
|
||||||
this.queueJobForIdentifier(identifier, async () =>
|
this.queueJobForIdentifier(identifier, async () =>
|
||||||
|
@ -992,6 +1012,8 @@ export default class MessageSender {
|
||||||
|
|
||||||
// Support for sync messages
|
// Support for sync messages
|
||||||
|
|
||||||
|
// Note: this is used for sending real messages to your other devices after sending a
|
||||||
|
// message to others.
|
||||||
async sendSyncMessage({
|
async sendSyncMessage({
|
||||||
encodedDataMessage,
|
encodedDataMessage,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
@ -1012,14 +1034,9 @@ export default class MessageSender {
|
||||||
unidentifiedDeliveries?: Array<string>;
|
unidentifiedDeliveries?: Array<string>;
|
||||||
isUpdate?: boolean;
|
isUpdate?: boolean;
|
||||||
options?: SendOptionsType;
|
options?: SendOptionsType;
|
||||||
}): Promise<CallbackResultType | void> {
|
}): Promise<CallbackResultType> {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
|
||||||
|
|
||||||
if (myDevice === 1) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataMessage = Proto.DataMessage.decode(
|
const dataMessage = Proto.DataMessage.decode(
|
||||||
new FIXMEU8(encodedDataMessage)
|
new FIXMEU8(encodedDataMessage)
|
||||||
|
@ -1082,134 +1099,112 @@ export default class MessageSender {
|
||||||
identifier: myUuid || myNumber,
|
identifier: myUuid || myNumber,
|
||||||
proto: contentMessage,
|
proto: contentMessage,
|
||||||
timestamp,
|
timestamp,
|
||||||
contentHint: ContentHint.IMPLICIT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendRequestBlockSyncMessage(
|
async sendRequestBlockSyncMessage(
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType | void> {
|
): Promise<CallbackResultType> {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
|
||||||
if (myDevice !== 1) {
|
|
||||||
const request = new Proto.SyncMessage.Request();
|
|
||||||
request.type = Proto.SyncMessage.Request.Type.BLOCKED;
|
|
||||||
const syncMessage = this.createSyncMessage();
|
|
||||||
syncMessage.request = request;
|
|
||||||
const contentMessage = new Proto.Content();
|
|
||||||
contentMessage.syncMessage = syncMessage;
|
|
||||||
|
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const request = new Proto.SyncMessage.Request();
|
||||||
|
request.type = Proto.SyncMessage.Request.Type.BLOCKED;
|
||||||
|
const syncMessage = this.createSyncMessage();
|
||||||
|
syncMessage.request = request;
|
||||||
|
const contentMessage = new Proto.Content();
|
||||||
|
contentMessage.syncMessage = syncMessage;
|
||||||
|
|
||||||
return this.sendIndividualProto({
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
identifier: myUuid || myNumber,
|
|
||||||
proto: contentMessage,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
contentHint: ContentHint.IMPLICIT,
|
|
||||||
options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
return this.sendIndividualProto({
|
||||||
|
identifier: myUuid || myNumber,
|
||||||
|
proto: contentMessage,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
contentHint: ContentHint.IMPLICIT,
|
||||||
|
options,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendRequestConfigurationSyncMessage(
|
async sendRequestConfigurationSyncMessage(
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType | void> {
|
): Promise<CallbackResultType> {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
|
||||||
if (myDevice !== 1) {
|
|
||||||
const request = new Proto.SyncMessage.Request();
|
|
||||||
request.type = Proto.SyncMessage.Request.Type.CONFIGURATION;
|
|
||||||
const syncMessage = this.createSyncMessage();
|
|
||||||
syncMessage.request = request;
|
|
||||||
const contentMessage = new Proto.Content();
|
|
||||||
contentMessage.syncMessage = syncMessage;
|
|
||||||
|
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const request = new Proto.SyncMessage.Request();
|
||||||
|
request.type = Proto.SyncMessage.Request.Type.CONFIGURATION;
|
||||||
|
const syncMessage = this.createSyncMessage();
|
||||||
|
syncMessage.request = request;
|
||||||
|
const contentMessage = new Proto.Content();
|
||||||
|
contentMessage.syncMessage = syncMessage;
|
||||||
|
|
||||||
return this.sendIndividualProto({
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
identifier: myUuid || myNumber,
|
|
||||||
proto: contentMessage,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
contentHint: ContentHint.IMPLICIT,
|
|
||||||
options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
return this.sendIndividualProto({
|
||||||
|
identifier: myUuid || myNumber,
|
||||||
|
proto: contentMessage,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
contentHint: ContentHint.IMPLICIT,
|
||||||
|
options,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendRequestGroupSyncMessage(
|
async sendRequestGroupSyncMessage(
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType | void> {
|
): Promise<CallbackResultType> {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
|
||||||
if (myDevice !== 1) {
|
|
||||||
const request = new Proto.SyncMessage.Request();
|
|
||||||
request.type = Proto.SyncMessage.Request.Type.GROUPS;
|
|
||||||
const syncMessage = this.createSyncMessage();
|
|
||||||
syncMessage.request = request;
|
|
||||||
const contentMessage = new Proto.Content();
|
|
||||||
contentMessage.syncMessage = syncMessage;
|
|
||||||
|
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const request = new Proto.SyncMessage.Request();
|
||||||
|
request.type = Proto.SyncMessage.Request.Type.GROUPS;
|
||||||
|
const syncMessage = this.createSyncMessage();
|
||||||
|
syncMessage.request = request;
|
||||||
|
const contentMessage = new Proto.Content();
|
||||||
|
contentMessage.syncMessage = syncMessage;
|
||||||
|
|
||||||
return this.sendIndividualProto({
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
identifier: myUuid || myNumber,
|
|
||||||
proto: contentMessage,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
contentHint: ContentHint.IMPLICIT,
|
|
||||||
options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
return this.sendIndividualProto({
|
||||||
|
identifier: myUuid || myNumber,
|
||||||
|
proto: contentMessage,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
contentHint: ContentHint.IMPLICIT,
|
||||||
|
options,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendRequestContactSyncMessage(
|
async sendRequestContactSyncMessage(
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType | void> {
|
): Promise<CallbackResultType> {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
|
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
const request = new Proto.SyncMessage.Request();
|
||||||
if (myDevice !== 1) {
|
request.type = Proto.SyncMessage.Request.Type.CONTACTS;
|
||||||
const request = new Proto.SyncMessage.Request();
|
const syncMessage = this.createSyncMessage();
|
||||||
request.type = Proto.SyncMessage.Request.Type.CONTACTS;
|
syncMessage.request = request;
|
||||||
const syncMessage = this.createSyncMessage();
|
const contentMessage = new Proto.Content();
|
||||||
syncMessage.request = request;
|
contentMessage.syncMessage = syncMessage;
|
||||||
const contentMessage = new Proto.Content();
|
|
||||||
contentMessage.syncMessage = syncMessage;
|
|
||||||
|
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
|
|
||||||
return this.sendIndividualProto({
|
return this.sendIndividualProto({
|
||||||
identifier: myUuid || myNumber,
|
identifier: myUuid || myNumber,
|
||||||
proto: contentMessage,
|
proto: contentMessage,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
contentHint: ContentHint.IMPLICIT,
|
contentHint: ContentHint.IMPLICIT,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendFetchManifestSyncMessage(
|
async sendFetchManifestSyncMessage(
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType | void> {
|
): Promise<CallbackResultType> {
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
|
||||||
|
|
||||||
if (myDevice === 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchLatest = new Proto.SyncMessage.FetchLatest();
|
const fetchLatest = new Proto.SyncMessage.FetchLatest();
|
||||||
fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST;
|
fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST;
|
||||||
|
@ -1221,7 +1216,7 @@ export default class MessageSender {
|
||||||
|
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
|
|
||||||
await this.sendIndividualProto({
|
return this.sendIndividualProto({
|
||||||
identifier: myUuid || myNumber,
|
identifier: myUuid || myNumber,
|
||||||
proto: contentMessage,
|
proto: contentMessage,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
@ -1232,14 +1227,9 @@ export default class MessageSender {
|
||||||
|
|
||||||
async sendRequestKeySyncMessage(
|
async sendRequestKeySyncMessage(
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType | void> {
|
): Promise<CallbackResultType> {
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
|
||||||
|
|
||||||
if (myDevice === 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = new Proto.SyncMessage.Request();
|
const request = new Proto.SyncMessage.Request();
|
||||||
request.type = Proto.SyncMessage.Request.Type.KEYS;
|
request.type = Proto.SyncMessage.Request.Type.KEYS;
|
||||||
|
@ -1251,7 +1241,7 @@ export default class MessageSender {
|
||||||
|
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
|
|
||||||
await this.sendIndividualProto({
|
return this.sendIndividualProto({
|
||||||
identifier: myUuid || myNumber,
|
identifier: myUuid || myNumber,
|
||||||
proto: contentMessage,
|
proto: contentMessage,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
@ -1267,13 +1257,10 @@ export default class MessageSender {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}>,
|
}>,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType | void> {
|
): Promise<CallbackResultType> {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
|
||||||
if (myDevice === 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const syncMessage = this.createSyncMessage();
|
const syncMessage = this.createSyncMessage();
|
||||||
syncMessage.read = [];
|
syncMessage.read = [];
|
||||||
for (let i = 0; i < reads.length; i += 1) {
|
for (let i = 0; i < reads.length; i += 1) {
|
||||||
|
@ -1290,7 +1277,7 @@ export default class MessageSender {
|
||||||
identifier: myUuid || myNumber,
|
identifier: myUuid || myNumber,
|
||||||
proto: contentMessage,
|
proto: contentMessage,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
contentHint: ContentHint.IMPLICIT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1300,13 +1287,9 @@ export default class MessageSender {
|
||||||
senderUuid: string,
|
senderUuid: string,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType | null> {
|
): Promise<CallbackResultType> {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
|
||||||
if (myDevice === 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncMessage = this.createSyncMessage();
|
const syncMessage = this.createSyncMessage();
|
||||||
|
|
||||||
|
@ -1327,7 +1310,7 @@ export default class MessageSender {
|
||||||
identifier: myUuid || myNumber,
|
identifier: myUuid || myNumber,
|
||||||
proto: contentMessage,
|
proto: contentMessage,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
contentHint: ContentHint.IMPLICIT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1340,13 +1323,9 @@ export default class MessageSender {
|
||||||
type: number;
|
type: number;
|
||||||
},
|
},
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType | null> {
|
): Promise<CallbackResultType> {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
|
||||||
if (myDevice === 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncMessage = this.createSyncMessage();
|
const syncMessage = this.createSyncMessage();
|
||||||
|
|
||||||
|
@ -1372,7 +1351,7 @@ export default class MessageSender {
|
||||||
identifier: myUuid || myNumber,
|
identifier: myUuid || myNumber,
|
||||||
proto: contentMessage,
|
proto: contentMessage,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
contentHint: ContentHint.IMPLICIT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1384,12 +1363,7 @@ export default class MessageSender {
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
}>,
|
}>,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType | null> {
|
): Promise<CallbackResultType> {
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
|
||||||
if (myDevice === 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
const ENUM = Proto.SyncMessage.StickerPackOperation.Type;
|
const ENUM = Proto.SyncMessage.StickerPackOperation.Type;
|
||||||
|
@ -1423,57 +1397,60 @@ export default class MessageSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncVerification(
|
async syncVerification(
|
||||||
destinationE164: string,
|
destinationE164: string | undefined,
|
||||||
destinationUuid: string,
|
destinationUuid: string | undefined,
|
||||||
state: number,
|
state: number,
|
||||||
identityKey: ArrayBuffer,
|
identityKey: ArrayBuffer,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType | void> {
|
): Promise<CallbackResultType> {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (myDevice === 1) {
|
if (!destinationE164 && !destinationUuid) {
|
||||||
return Promise.resolve();
|
throw new Error('syncVerification: Neither e164 nor UUID were provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get padding which we can share between null message and verified sync
|
// Get padding which we can share between null message and verified sync
|
||||||
const padding = this.getRandomPadding();
|
const padding = this.getRandomPadding();
|
||||||
|
|
||||||
// First send a null message to mask the sync message.
|
// First send a null message to mask the sync message.
|
||||||
const promise = this.sendNullMessage(
|
await handleMessageSend(
|
||||||
{ uuid: destinationUuid, e164: destinationE164, padding },
|
this.sendNullMessage(
|
||||||
options
|
{ uuid: destinationUuid, e164: destinationE164, padding },
|
||||||
|
options
|
||||||
|
),
|
||||||
|
{
|
||||||
|
messageIds: [],
|
||||||
|
sendType: 'nullMessage',
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return promise.then(async () => {
|
const verified = new Proto.Verified();
|
||||||
const verified = new Proto.Verified();
|
verified.state = state;
|
||||||
verified.state = state;
|
if (destinationE164) {
|
||||||
if (destinationE164) {
|
verified.destination = destinationE164;
|
||||||
verified.destination = destinationE164;
|
}
|
||||||
}
|
if (destinationUuid) {
|
||||||
if (destinationUuid) {
|
verified.destinationUuid = destinationUuid;
|
||||||
verified.destinationUuid = destinationUuid;
|
}
|
||||||
}
|
verified.identityKey = new FIXMEU8(identityKey);
|
||||||
verified.identityKey = new FIXMEU8(identityKey);
|
verified.nullMessage = padding;
|
||||||
verified.nullMessage = padding;
|
|
||||||
|
|
||||||
const syncMessage = this.createSyncMessage();
|
const syncMessage = this.createSyncMessage();
|
||||||
syncMessage.verified = verified;
|
syncMessage.verified = verified;
|
||||||
|
|
||||||
const secondMessage = new Proto.Content();
|
const secondMessage = new Proto.Content();
|
||||||
secondMessage.syncMessage = syncMessage;
|
secondMessage.syncMessage = syncMessage;
|
||||||
|
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
|
|
||||||
await this.sendIndividualProto({
|
return this.sendIndividualProto({
|
||||||
identifier: myUuid || myNumber,
|
identifier: myUuid || myNumber,
|
||||||
proto: secondMessage,
|
proto: secondMessage,
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
contentHint: ContentHint.IMPLICIT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
options,
|
options,
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1512,7 +1489,7 @@ export default class MessageSender {
|
||||||
recipientId: string,
|
recipientId: string,
|
||||||
callingMessage: Proto.ICallingMessage,
|
callingMessage: Proto.ICallingMessage,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<void> {
|
): Promise<CallbackResultType> {
|
||||||
const recipients = [recipientId];
|
const recipients = [recipientId];
|
||||||
const finalTimestamp = Date.now();
|
const finalTimestamp = Date.now();
|
||||||
|
|
||||||
|
@ -1521,7 +1498,7 @@ export default class MessageSender {
|
||||||
|
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
|
|
||||||
await this.sendMessageProtoAndWait({
|
return this.sendMessageProtoAndWait({
|
||||||
timestamp: finalTimestamp,
|
timestamp: finalTimestamp,
|
||||||
recipients,
|
recipients,
|
||||||
proto: contentMessage,
|
proto: contentMessage,
|
||||||
|
@ -1537,16 +1514,15 @@ export default class MessageSender {
|
||||||
timestamps,
|
timestamps,
|
||||||
options,
|
options,
|
||||||
}: {
|
}: {
|
||||||
e164: string;
|
e164?: string;
|
||||||
uuid: string;
|
uuid?: string;
|
||||||
timestamps: Array<number>;
|
timestamps: Array<number>;
|
||||||
options?: SendOptionsType;
|
options?: SendOptionsType;
|
||||||
}): Promise<CallbackResultType | void> {
|
}): Promise<CallbackResultType> {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
if (!uuid && !e164) {
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
throw new Error(
|
||||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
'sendDeliveryReceipt: Neither uuid nor e164 was provided!'
|
||||||
if ((myNumber === e164 || myUuid === uuid) && myDevice === 1) {
|
);
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const receiptMessage = new Proto.ReceiptMessage();
|
const receiptMessage = new Proto.ReceiptMessage();
|
||||||
|
@ -1562,7 +1538,7 @@ export default class MessageSender {
|
||||||
identifier: uuid || e164,
|
identifier: uuid || e164,
|
||||||
proto: contentMessage,
|
proto: contentMessage,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
contentHint: ContentHint.IMPLICIT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1591,7 +1567,7 @@ export default class MessageSender {
|
||||||
identifier: senderUuid || senderE164,
|
identifier: senderUuid || senderE164,
|
||||||
proto: contentMessage,
|
proto: contentMessage,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
contentHint: ContentHint.IMPLICIT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1634,9 +1610,7 @@ export default class MessageSender {
|
||||||
e164: string,
|
e164: string,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<
|
): Promise<CallbackResultType> {
|
||||||
CallbackResultType | void | Array<CallbackResultType | void | Array<void>>
|
|
||||||
> {
|
|
||||||
window.log.info('resetSession: start');
|
window.log.info('resetSession: start');
|
||||||
const proto = new Proto.DataMessage();
|
const proto = new Proto.DataMessage();
|
||||||
proto.body = 'TERMINATE';
|
proto.body = 'TERMINATE';
|
||||||
|
@ -1659,19 +1633,27 @@ export default class MessageSender {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'resetSession: finished closing local sessions, now sending to contact'
|
'resetSession: finished closing local sessions, now sending to contact'
|
||||||
);
|
);
|
||||||
return this.sendIndividualProto({
|
return handleMessageSend(
|
||||||
identifier,
|
this.sendIndividualProto({
|
||||||
proto,
|
identifier,
|
||||||
timestamp,
|
proto,
|
||||||
contentHint: ContentHint.DEFAULT,
|
timestamp,
|
||||||
options,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
}).catch(logError('resetSession/sendToContact error:'));
|
options,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
messageIds: [],
|
||||||
|
sendType: 'resetSession',
|
||||||
|
}
|
||||||
|
).catch(logError('resetSession/sendToContact error:'));
|
||||||
})
|
})
|
||||||
.then(async () =>
|
.then(async result => {
|
||||||
window.textsecure.storage.protocol
|
await window.textsecure.storage.protocol
|
||||||
.archiveAllSessions(identifier)
|
.archiveAllSessions(identifier)
|
||||||
.catch(logError('resetSession/archiveAllSessions2 error:'))
|
.catch(logError('resetSession/archiveAllSessions2 error:'));
|
||||||
);
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
|
@ -1694,7 +1676,12 @@ export default class MessageSender {
|
||||||
options,
|
options,
|
||||||
}).catch(logError('resetSession/sendSync error:'));
|
}).catch(logError('resetSession/sendSync error:'));
|
||||||
|
|
||||||
return Promise.all([sendToContactPromise, sendSyncPromise]);
|
const responses = await Promise.all([
|
||||||
|
sendToContactPromise,
|
||||||
|
sendSyncPromise,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return responses[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendExpirationTimerUpdateToIdentifier(
|
async sendExpirationTimerUpdateToIdentifier(
|
||||||
|
@ -1714,17 +1701,19 @@ export default class MessageSender {
|
||||||
profileKey,
|
profileKey,
|
||||||
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||||
},
|
},
|
||||||
contentHint: ContentHint.DEFAULT,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
groupId: undefined,
|
groupId: undefined,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendRetryRequest({
|
async sendRetryRequest({
|
||||||
|
groupId,
|
||||||
options,
|
options,
|
||||||
plaintext,
|
plaintext,
|
||||||
uuid,
|
uuid,
|
||||||
}: {
|
}: {
|
||||||
|
groupId?: string;
|
||||||
options?: SendOptionsType;
|
options?: SendOptionsType;
|
||||||
plaintext: PlaintextContent;
|
plaintext: PlaintextContent;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
@ -1735,29 +1724,99 @@ export default class MessageSender {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
recipients: [uuid],
|
recipients: [uuid],
|
||||||
proto: plaintext,
|
proto: plaintext,
|
||||||
contentHint: ContentHint.IMPLICIT,
|
contentHint: ContentHint.DEFAULT,
|
||||||
groupId: undefined,
|
groupId,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group sends
|
// Group sends
|
||||||
|
|
||||||
|
// Used to ensure that when we send to a group the old way, we save to the send log as
|
||||||
|
// we send to each recipient. Then we don't have a long delay between the first send
|
||||||
|
// and the final save to the database with all recipients.
|
||||||
|
makeSendLogCallback({
|
||||||
|
contentHint,
|
||||||
|
messageId,
|
||||||
|
proto,
|
||||||
|
sendType,
|
||||||
|
timestamp,
|
||||||
|
}: {
|
||||||
|
contentHint: number;
|
||||||
|
messageId?: string;
|
||||||
|
proto: Buffer;
|
||||||
|
sendType: SendTypesType;
|
||||||
|
timestamp: number;
|
||||||
|
}): SendLogCallbackType {
|
||||||
|
let initialSavePromise: Promise<number>;
|
||||||
|
|
||||||
|
return async ({
|
||||||
|
identifier,
|
||||||
|
deviceIds,
|
||||||
|
}: {
|
||||||
|
identifier: string;
|
||||||
|
deviceIds: Array<number>;
|
||||||
|
}) => {
|
||||||
|
if (!shouldSaveProto(sendType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.get(identifier);
|
||||||
|
if (!conversation) {
|
||||||
|
window.log.warn(
|
||||||
|
`makeSendLogCallback: Unable to find conversation for identifier ${identifier}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const recipientUuid = conversation.get('uuid');
|
||||||
|
if (!recipientUuid) {
|
||||||
|
window.log.warn(
|
||||||
|
`makeSendLogCallback: Conversation ${conversation.idForLogging()} had no UUID`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initialSavePromise) {
|
||||||
|
initialSavePromise = window.Signal.Data.insertSentProto(
|
||||||
|
{
|
||||||
|
timestamp,
|
||||||
|
proto,
|
||||||
|
contentHint,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recipients: { [recipientUuid]: deviceIds },
|
||||||
|
messageIds: messageId ? [messageId] : [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await initialSavePromise;
|
||||||
|
} else {
|
||||||
|
const id = await initialSavePromise;
|
||||||
|
await window.Signal.Data.insertProtoRecipients({
|
||||||
|
id,
|
||||||
|
recipientUuid,
|
||||||
|
deviceIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// No functions should really call this; since most group sends are now via Sender Key
|
// No functions should really call this; since most group sends are now via Sender Key
|
||||||
async sendGroupProto({
|
async sendGroupProto({
|
||||||
recipients,
|
|
||||||
proto,
|
|
||||||
timestamp = Date.now(),
|
|
||||||
contentHint,
|
contentHint,
|
||||||
groupId,
|
groupId,
|
||||||
options,
|
options,
|
||||||
|
proto,
|
||||||
|
recipients,
|
||||||
|
sendLogCallback,
|
||||||
|
timestamp = Date.now(),
|
||||||
}: {
|
}: {
|
||||||
recipients: Array<string>;
|
|
||||||
proto: Proto.Content;
|
|
||||||
timestamp: number;
|
|
||||||
contentHint: number;
|
contentHint: number;
|
||||||
groupId: string | undefined;
|
groupId: string | undefined;
|
||||||
options?: SendOptionsType;
|
options?: SendOptionsType;
|
||||||
|
proto: Proto.Content;
|
||||||
|
recipients: Array<string>;
|
||||||
|
sendLogCallback?: SendLogCallbackType;
|
||||||
|
timestamp: number;
|
||||||
}): Promise<CallbackResultType> {
|
}): Promise<CallbackResultType> {
|
||||||
const dataMessage = proto.dataMessage
|
const dataMessage = proto.dataMessage
|
||||||
? typedArrayToArrayBuffer(
|
? typedArrayToArrayBuffer(
|
||||||
|
@ -1790,13 +1849,14 @@ export default class MessageSender {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sendMessageProto({
|
this.sendMessageProto({
|
||||||
timestamp,
|
callback,
|
||||||
recipients: identifiers,
|
|
||||||
proto,
|
|
||||||
contentHint,
|
contentHint,
|
||||||
groupId,
|
groupId,
|
||||||
callback,
|
|
||||||
options,
|
options,
|
||||||
|
proto,
|
||||||
|
recipients: identifiers,
|
||||||
|
sendLogCallback,
|
||||||
|
timestamp,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1846,19 +1906,31 @@ export default class MessageSender {
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType> {
|
): Promise<CallbackResultType> {
|
||||||
const contentMessage = new Proto.Content();
|
const contentMessage = new Proto.Content();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
const senderKeyDistributionMessage = await this.getSenderKeyDistributionMessage(
|
const senderKeyDistributionMessage = await this.getSenderKeyDistributionMessage(
|
||||||
distributionId
|
distributionId
|
||||||
);
|
);
|
||||||
contentMessage.senderKeyDistributionMessage = senderKeyDistributionMessage.serialize();
|
contentMessage.senderKeyDistributionMessage = senderKeyDistributionMessage.serialize();
|
||||||
|
|
||||||
|
const sendLogCallback =
|
||||||
|
identifiers.length > 1
|
||||||
|
? this.makeSendLogCallback({
|
||||||
|
contentHint,
|
||||||
|
proto: Buffer.from(Proto.Content.encode(contentMessage).finish()),
|
||||||
|
sendType: 'senderKeyDistributionMessage',
|
||||||
|
timestamp,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return this.sendGroupProto({
|
return this.sendGroupProto({
|
||||||
recipients: identifiers,
|
|
||||||
proto: contentMessage,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
contentHint,
|
contentHint,
|
||||||
groupId,
|
groupId,
|
||||||
options,
|
options,
|
||||||
|
proto: contentMessage,
|
||||||
|
recipients: identifiers,
|
||||||
|
sendLogCallback,
|
||||||
|
timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1869,6 +1941,7 @@ export default class MessageSender {
|
||||||
groupIdentifiers: Array<string>,
|
groupIdentifiers: Array<string>,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType> {
|
): Promise<CallbackResultType> {
|
||||||
|
const timestamp = Date.now();
|
||||||
const proto = new Proto.Content({
|
const proto = new Proto.Content({
|
||||||
dataMessage: {
|
dataMessage: {
|
||||||
group: {
|
group: {
|
||||||
|
@ -1879,13 +1952,26 @@ export default class MessageSender {
|
||||||
});
|
});
|
||||||
|
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
|
|
||||||
|
const contentHint = ContentHint.RESENDABLE;
|
||||||
|
const sendLogCallback =
|
||||||
|
groupIdentifiers.length > 1
|
||||||
|
? this.makeSendLogCallback({
|
||||||
|
contentHint,
|
||||||
|
proto: Buffer.from(Proto.Content.encode(proto).finish()),
|
||||||
|
sendType: 'legacyGroupChange',
|
||||||
|
timestamp,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return this.sendGroupProto({
|
return this.sendGroupProto({
|
||||||
recipients: groupIdentifiers,
|
contentHint,
|
||||||
proto,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
contentHint: ContentHint.DEFAULT,
|
|
||||||
groupId: undefined, // only for GV2 ids
|
groupId: undefined, // only for GV2 ids
|
||||||
options,
|
options,
|
||||||
|
proto,
|
||||||
|
recipients: groupIdentifiers,
|
||||||
|
sendLogCallback,
|
||||||
|
timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1913,6 +1999,7 @@ export default class MessageSender {
|
||||||
type: Proto.GroupContext.Type.DELIVER,
|
type: Proto.GroupContext.Type.DELIVER,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const proto = await this.getContentMessage(messageOptions);
|
||||||
|
|
||||||
if (recipients.length === 0) {
|
if (recipients.length === 0) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
|
@ -1925,11 +2012,25 @@ export default class MessageSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
return this.sendMessage({
|
const contentHint = ContentHint.RESENDABLE;
|
||||||
messageOptions,
|
const sendLogCallback =
|
||||||
contentHint: ContentHint.DEFAULT,
|
groupIdentifiers.length > 1
|
||||||
|
? this.makeSendLogCallback({
|
||||||
|
contentHint,
|
||||||
|
proto: Buffer.from(Proto.Content.encode(proto).finish()),
|
||||||
|
sendType: 'expirationTimerUpdate',
|
||||||
|
timestamp,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return this.sendGroupProto({
|
||||||
|
contentHint,
|
||||||
groupId: undefined, // only for GV2 ids
|
groupId: undefined, // only for GV2 ids
|
||||||
options,
|
options,
|
||||||
|
proto,
|
||||||
|
recipients,
|
||||||
|
sendLogCallback,
|
||||||
|
timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ import MessageReceiver from './MessageReceiver';
|
||||||
import { ContactSyncEvent, GroupSyncEvent } from './messageReceiverEvents';
|
import { ContactSyncEvent, GroupSyncEvent } from './messageReceiverEvents';
|
||||||
import MessageSender from './SendMessage';
|
import MessageSender from './SendMessage';
|
||||||
import { assert } from '../util/assert';
|
import { assert } from '../util/assert';
|
||||||
|
import { getSendOptions } from '../util/getSendOptions';
|
||||||
|
import { handleMessageSend } from '../util/handleMessageSend';
|
||||||
|
|
||||||
class SyncRequestInner extends EventTarget {
|
class SyncRequestInner extends EventTarget {
|
||||||
private started = false;
|
private started = false;
|
||||||
|
@ -61,25 +63,41 @@ class SyncRequestInner extends EventTarget {
|
||||||
|
|
||||||
const { sender } = this;
|
const { sender } = this;
|
||||||
|
|
||||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||||
const {
|
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||||
wrap,
|
|
||||||
sendOptions,
|
|
||||||
} = await window.ConversationController.prepareForSend(ourNumber, {
|
|
||||||
syncMessage: true,
|
syncMessage: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
window.log.warn(
|
||||||
|
'SyncRequest.start: We are primary device; returning early'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
window.log.info('SyncRequest created. Sending config sync request...');
|
window.log.info('SyncRequest created. Sending config sync request...');
|
||||||
wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));
|
handleMessageSend(sender.sendRequestConfigurationSyncMessage(sendOptions), {
|
||||||
|
messageIds: [],
|
||||||
|
sendType: 'otherSync',
|
||||||
|
});
|
||||||
|
|
||||||
window.log.info('SyncRequest now sending block sync request...');
|
window.log.info('SyncRequest now sending block sync request...');
|
||||||
wrap(sender.sendRequestBlockSyncMessage(sendOptions));
|
handleMessageSend(sender.sendRequestBlockSyncMessage(sendOptions), {
|
||||||
|
messageIds: [],
|
||||||
|
sendType: 'otherSync',
|
||||||
|
});
|
||||||
|
|
||||||
window.log.info('SyncRequest now sending contact sync message...');
|
window.log.info('SyncRequest now sending contact sync message...');
|
||||||
wrap(sender.sendRequestContactSyncMessage(sendOptions))
|
handleMessageSend(sender.sendRequestContactSyncMessage(sendOptions), {
|
||||||
|
messageIds: [],
|
||||||
|
sendType: 'otherSync',
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
window.log.info('SyncRequest now sending group sync message...');
|
window.log.info('SyncRequest now sending group sync message...');
|
||||||
return wrap(sender.sendRequestGroupSyncMessage(sendOptions));
|
return handleMessageSend(
|
||||||
|
sender.sendRequestGroupSyncMessage(sendOptions),
|
||||||
|
{ messageIds: [], sendType: 'otherSync' }
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
|
|
1
ts/textsecure/Types.d.ts
vendored
1
ts/textsecure/Types.d.ts
vendored
|
@ -75,6 +75,7 @@ export type ProcessedEnvelope = Readonly<{
|
||||||
content?: Uint8Array;
|
content?: Uint8Array;
|
||||||
serverGuid: string;
|
serverGuid: string;
|
||||||
serverTimestamp: number;
|
serverTimestamp: number;
|
||||||
|
groupId?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ProcessedAttachment = {
|
export type ProcessedAttachment = {
|
||||||
|
|
|
@ -219,6 +219,7 @@ export type ReadEventData = Readonly<{
|
||||||
envelopeTimestamp: number;
|
envelopeTimestamp: number;
|
||||||
source?: string;
|
source?: string;
|
||||||
sourceUuid?: string;
|
sourceUuid?: string;
|
||||||
|
sourceDevice?: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export class ReadEvent extends ConfirmableEvent {
|
export class ReadEvent extends ConfirmableEvent {
|
||||||
|
|
|
@ -5,6 +5,7 @@ export type ReactionType = Readonly<{
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
emoji: string;
|
emoji: string;
|
||||||
fromId: string;
|
fromId: string;
|
||||||
|
messageId: string | undefined;
|
||||||
messageReceivedAt: number;
|
messageReceivedAt: number;
|
||||||
targetAuthorUuid: string;
|
targetAuthorUuid: string;
|
||||||
targetTimestamp: number;
|
targetTimestamp: number;
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
import { CallbackResultType } from '../textsecure/SendMessage';
|
import { CallbackResultType } from '../textsecure/SendMessage';
|
||||||
|
import dataInterface from '../sql/Client';
|
||||||
|
|
||||||
|
const { insertSentProto } = dataInterface;
|
||||||
|
|
||||||
export const SEALED_SENDER = {
|
export const SEALED_SENDER = {
|
||||||
UNKNOWN: 0,
|
UNKNOWN: 0,
|
||||||
|
@ -10,17 +14,71 @@ export const SEALED_SENDER = {
|
||||||
UNRESTRICTED: 3,
|
UNRESTRICTED: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SendTypesType =
|
||||||
|
| 'callingMessage' // excluded from send log
|
||||||
|
| 'deleteForEveryone'
|
||||||
|
| 'deliveryReceipt'
|
||||||
|
| 'expirationTimerUpdate'
|
||||||
|
| 'groupChange'
|
||||||
|
| 'legacyGroupChange'
|
||||||
|
| 'message'
|
||||||
|
| 'messageRetry'
|
||||||
|
| 'nullMessage' // excluded from send log
|
||||||
|
| 'otherSync'
|
||||||
|
| 'profileKeyUpdate'
|
||||||
|
| 'reaction'
|
||||||
|
| 'readReceipt'
|
||||||
|
| 'readSync'
|
||||||
|
| 'resendFromLog' // excluded from send log
|
||||||
|
| 'resetSession'
|
||||||
|
| 'retryRequest' // excluded from send log
|
||||||
|
| 'senderKeyDistributionMessage'
|
||||||
|
| 'sentSync'
|
||||||
|
| 'typing' // excluded from send log
|
||||||
|
| 'verificationSync'
|
||||||
|
| 'viewOnceSync';
|
||||||
|
|
||||||
|
export function shouldSaveProto(sendType: SendTypesType): boolean {
|
||||||
|
if (sendType === 'callingMessage') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendType === 'nullMessage') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendType === 'resendFromLog') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendType === 'retryRequest') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendType === 'typing') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleMessageSend(
|
export async function handleMessageSend(
|
||||||
promise: Promise<CallbackResultType | void | null>
|
promise: Promise<CallbackResultType>,
|
||||||
): Promise<CallbackResultType | void | null> {
|
options: {
|
||||||
|
messageIds: Array<string>;
|
||||||
|
sendType: SendTypesType;
|
||||||
|
}
|
||||||
|
): Promise<CallbackResultType> {
|
||||||
try {
|
try {
|
||||||
const result = await promise;
|
const result = await promise;
|
||||||
if (result) {
|
|
||||||
await handleMessageSendResult(
|
await maybeSaveToSendLog(result, options);
|
||||||
result.failoverIdentifiers,
|
|
||||||
result.unidentifiedDeliveries
|
await handleMessageSendResult(
|
||||||
);
|
result.failoverIdentifiers,
|
||||||
}
|
result.unidentifiedDeliveries
|
||||||
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -84,3 +142,52 @@ async function handleMessageSendResult(
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function maybeSaveToSendLog(
|
||||||
|
result: CallbackResultType,
|
||||||
|
{
|
||||||
|
messageIds,
|
||||||
|
sendType,
|
||||||
|
}: {
|
||||||
|
messageIds: Array<string>;
|
||||||
|
sendType: SendTypesType;
|
||||||
|
}
|
||||||
|
): Promise<void> {
|
||||||
|
const { contentHint, contentProto, recipients, timestamp } = result;
|
||||||
|
|
||||||
|
if (!shouldSaveProto(sendType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNumber(contentHint) || !contentProto || !recipients || !timestamp) {
|
||||||
|
window.log.warn(
|
||||||
|
`handleMessageSend: Missing necessary information to save to log for ${sendType} message ${timestamp}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identifiers = Object.keys(recipients);
|
||||||
|
if (identifiers.length === 0) {
|
||||||
|
window.log.warn(
|
||||||
|
`handleMessageSend: ${sendType} message ${timestamp} had no recipients`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the identifier count is greater than one, we've done the save elsewhere
|
||||||
|
if (identifiers.length > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await insertSentProto(
|
||||||
|
{
|
||||||
|
timestamp,
|
||||||
|
proto: Buffer.from(contentProto),
|
||||||
|
contentHint,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
messageIds,
|
||||||
|
recipients,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
520
ts/util/handleRetry.ts
Normal file
520
ts/util/handleRetry.ts
Normal file
|
@ -0,0 +1,520 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import {
|
||||||
|
DecryptionErrorMessage,
|
||||||
|
PlaintextContent,
|
||||||
|
} from '@signalapp/signal-client';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
|
import { assert } from './assert';
|
||||||
|
import { getSendOptions } from './getSendOptions';
|
||||||
|
import { handleMessageSend } from './handleMessageSend';
|
||||||
|
import { isGroupV2 } from './whatTypeOfConversation';
|
||||||
|
import { isOlderThan } from './timestamp';
|
||||||
|
import { parseIntOrThrow } from './parseIntOrThrow';
|
||||||
|
import * as RemoteConfig from '../RemoteConfig';
|
||||||
|
|
||||||
|
import { ConversationModel } from '../models/conversations';
|
||||||
|
import {
|
||||||
|
DecryptionErrorEvent,
|
||||||
|
DecryptionErrorEventData,
|
||||||
|
RetryRequestEvent,
|
||||||
|
RetryRequestEventData,
|
||||||
|
} from '../textsecure/messageReceiverEvents';
|
||||||
|
|
||||||
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
|
||||||
|
// Entrypoints
|
||||||
|
|
||||||
|
export async function onRetryRequest(event: RetryRequestEvent): Promise<void> {
|
||||||
|
const { retryRequest } = event;
|
||||||
|
const {
|
||||||
|
groupId: requestGroupId,
|
||||||
|
requesterDevice,
|
||||||
|
requesterUuid,
|
||||||
|
senderDevice,
|
||||||
|
sentAt,
|
||||||
|
} = retryRequest;
|
||||||
|
const logId = `${requesterUuid}.${requesterDevice} ${sentAt}-${senderDevice}`;
|
||||||
|
|
||||||
|
window.log.info(`onRetryRequest/${logId}: Starting...`);
|
||||||
|
|
||||||
|
if (window.RETRY_DELAY) {
|
||||||
|
window.log.warn(
|
||||||
|
`onRetryRequest/${logId}: Delaying because RETRY_DELAY is set...`
|
||||||
|
);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
}
|
||||||
|
|
||||||
|
const HOUR = 60 * 60 * 1000;
|
||||||
|
const ONE_DAY = 24 * HOUR;
|
||||||
|
let retryRespondMaxAge = ONE_DAY;
|
||||||
|
try {
|
||||||
|
retryRespondMaxAge = parseIntOrThrow(
|
||||||
|
RemoteConfig.getValue('desktop.retryRespondMaxAge'),
|
||||||
|
'retryRespondMaxAge'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
window.log.warn(
|
||||||
|
`onRetryRequest/${logId}: Failed to parse integer from desktop.retryRespondMaxAge feature flag`,
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOlderThan(sentAt, retryRespondMaxAge)) {
|
||||||
|
window.log.info(
|
||||||
|
`onRetryRequest/${logId}: Message is too old, refusing to send again.`
|
||||||
|
);
|
||||||
|
await sendDistributionMessageOrNullMessage(logId, retryRequest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentProto = await window.Signal.Data.getSentProtoByRecipient({
|
||||||
|
now: Date.now(),
|
||||||
|
recipientUuid: requesterUuid,
|
||||||
|
timestamp: sentAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sentProto) {
|
||||||
|
window.log.info(`onRetryRequest/${logId}: Did not find sent proto`);
|
||||||
|
await sendDistributionMessageOrNullMessage(logId, retryRequest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info(`onRetryRequest/${logId}: Resending message`);
|
||||||
|
await archiveSessionOnMatch(retryRequest);
|
||||||
|
|
||||||
|
const { contentHint, messageIds, proto, timestamp } = sentProto;
|
||||||
|
|
||||||
|
const { contentProto, groupId } = await maybeAddSenderKeyDistributionMessage({
|
||||||
|
contentProto: Proto.Content.decode(proto),
|
||||||
|
logId,
|
||||||
|
messageIds,
|
||||||
|
requestGroupId,
|
||||||
|
requesterUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recipientConversation = window.ConversationController.getOrCreate(
|
||||||
|
requesterUuid,
|
||||||
|
'private'
|
||||||
|
);
|
||||||
|
const sendOptions = await getSendOptions(recipientConversation.attributes);
|
||||||
|
const promise = window.textsecure.messaging.sendMessageProtoAndWait({
|
||||||
|
timestamp,
|
||||||
|
recipients: [requesterUuid],
|
||||||
|
proto: new Proto.Content(contentProto),
|
||||||
|
contentHint,
|
||||||
|
groupId,
|
||||||
|
options: sendOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleMessageSend(promise, {
|
||||||
|
messageIds: [],
|
||||||
|
sendType: 'resendFromLog',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeShowDecryptionToast(logId: string) {
|
||||||
|
if (!RemoteConfig.isEnabled('desktop.internalUser')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info(
|
||||||
|
`onDecryptionError/${logId}: Showing toast for internal user`
|
||||||
|
);
|
||||||
|
window.Whisper.ToastView.show(
|
||||||
|
window.Whisper.DecryptionErrorToast,
|
||||||
|
document.getElementsByClassName('conversation-stack')[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function onDecryptionError(
|
||||||
|
event: DecryptionErrorEvent
|
||||||
|
): Promise<void> {
|
||||||
|
const { decryptionError } = event;
|
||||||
|
const { senderUuid, senderDevice, timestamp } = decryptionError;
|
||||||
|
const logId = `${senderUuid}.${senderDevice} ${timestamp}`;
|
||||||
|
|
||||||
|
window.log.info(`onDecryptionError/${logId}: Starting...`);
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.getOrCreate(
|
||||||
|
senderUuid,
|
||||||
|
'private'
|
||||||
|
);
|
||||||
|
if (!conversation.get('capabilities')?.senderKey) {
|
||||||
|
await conversation.getProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversation.get('capabilities')?.senderKey) {
|
||||||
|
await requestResend(decryptionError);
|
||||||
|
} else {
|
||||||
|
await startAutomaticSessionReset(decryptionError);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info(`onDecryptionError/${logId}: ...complete`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
async function archiveSessionOnMatch({
|
||||||
|
requesterUuid,
|
||||||
|
requesterDevice,
|
||||||
|
senderDevice,
|
||||||
|
}: RetryRequestEventData): Promise<void> {
|
||||||
|
const ourDeviceId = parseIntOrThrow(
|
||||||
|
window.textsecure.storage.user.getDeviceId(),
|
||||||
|
'archiveSessionOnMatch/getDeviceId'
|
||||||
|
);
|
||||||
|
if (ourDeviceId === senderDevice) {
|
||||||
|
const address = `${requesterUuid}.${requesterDevice}`;
|
||||||
|
window.log.info('archiveSessionOnMatch: Devices match, archiving session');
|
||||||
|
await window.textsecure.storage.protocol.archiveSession(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendDistributionMessageOrNullMessage(
|
||||||
|
logId: string,
|
||||||
|
options: RetryRequestEventData
|
||||||
|
): Promise<void> {
|
||||||
|
const { groupId, requesterUuid } = options;
|
||||||
|
let sentDistributionMessage = false;
|
||||||
|
window.log.info(`sendDistributionMessageOrNullMessage/${logId}: Starting...`);
|
||||||
|
|
||||||
|
await archiveSessionOnMatch(options);
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.getOrCreate(
|
||||||
|
requesterUuid,
|
||||||
|
'private'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
const group = window.ConversationController.get(groupId);
|
||||||
|
const distributionId = group?.get('senderKeyInfo')?.distributionId;
|
||||||
|
|
||||||
|
if (group && !group.hasMember(requesterUuid)) {
|
||||||
|
throw new Error(
|
||||||
|
`sendDistributionMessageOrNullMessage/${logId}: Requester ${requesterUuid} is not a member of ${conversation.idForLogging()}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group && distributionId) {
|
||||||
|
window.log.info(
|
||||||
|
`sendDistributionMessageOrNullMessage/${logId}: Found matching group, sending sender key distribution message'`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
|
|
||||||
|
const result = await handleMessageSend(
|
||||||
|
window.textsecure.messaging.sendSenderKeyDistributionMessage({
|
||||||
|
contentHint: ContentHint.RESENDABLE,
|
||||||
|
distributionId,
|
||||||
|
groupId,
|
||||||
|
identifiers: [requesterUuid],
|
||||||
|
}),
|
||||||
|
{ messageIds: [], sendType: 'senderKeyDistributionMessage' }
|
||||||
|
);
|
||||||
|
if (result && result.errors && result.errors.length > 0) {
|
||||||
|
throw result.errors[0];
|
||||||
|
}
|
||||||
|
sentDistributionMessage = true;
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
`sendDistributionMessageOrNullMessage/${logId}: Failed to send sender key distribution message`,
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sentDistributionMessage) {
|
||||||
|
window.log.info(
|
||||||
|
`sendDistributionMessageOrNullMessage/${logId}: Did not send distribution message, sending null message`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sendOptions = await getSendOptions(conversation.attributes);
|
||||||
|
const result = await handleMessageSend(
|
||||||
|
window.textsecure.messaging.sendNullMessage(
|
||||||
|
{ uuid: requesterUuid },
|
||||||
|
sendOptions
|
||||||
|
),
|
||||||
|
{ messageIds: [], sendType: 'nullMessage' }
|
||||||
|
);
|
||||||
|
if (result && result.errors && result.errors.length > 0) {
|
||||||
|
throw result.errors[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
`maybeSendDistributionMessage/${logId}: Failed to send null message`,
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRetryConversation({
|
||||||
|
logId,
|
||||||
|
messageIds,
|
||||||
|
requestGroupId,
|
||||||
|
}: {
|
||||||
|
logId: string;
|
||||||
|
messageIds: Array<string>;
|
||||||
|
requestGroupId?: string;
|
||||||
|
}): Promise<ConversationModel | undefined> {
|
||||||
|
if (messageIds.length !== 1) {
|
||||||
|
// Fail over to requested groupId
|
||||||
|
return window.ConversationController.get(requestGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [messageId] = messageIds;
|
||||||
|
const message = await window.Signal.Data.getMessageById(messageId, {
|
||||||
|
Message: window.Whisper.Message,
|
||||||
|
});
|
||||||
|
if (!message) {
|
||||||
|
window.log.warn(
|
||||||
|
`maybeAddSenderKeyDistributionMessage/${logId}: Unable to find message ${messageId}`
|
||||||
|
);
|
||||||
|
// Fail over to requested groupId
|
||||||
|
return window.ConversationController.get(requestGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversationId = message.get('conversationId');
|
||||||
|
return window.ConversationController.get(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybeAddSenderKeyDistributionMessage({
|
||||||
|
contentProto,
|
||||||
|
logId,
|
||||||
|
messageIds,
|
||||||
|
requestGroupId,
|
||||||
|
requesterUuid,
|
||||||
|
}: {
|
||||||
|
contentProto: Proto.IContent;
|
||||||
|
logId: string;
|
||||||
|
messageIds: Array<string>;
|
||||||
|
requestGroupId?: string;
|
||||||
|
requesterUuid: string;
|
||||||
|
}): Promise<{ contentProto: Proto.IContent; groupId?: string }> {
|
||||||
|
const conversation = await getRetryConversation({
|
||||||
|
logId,
|
||||||
|
messageIds,
|
||||||
|
requestGroupId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
window.log.warn(
|
||||||
|
`maybeAddSenderKeyDistributionMessage/${logId}: Unable to find conversation`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
contentProto,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!conversation.hasMember(requesterUuid)) {
|
||||||
|
throw new Error(
|
||||||
|
`maybeAddSenderKeyDistributionMessage/${logId}: Recipient ${requesterUuid} is not a member of ${conversation.idForLogging()}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGroupV2(conversation.attributes)) {
|
||||||
|
return {
|
||||||
|
contentProto,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderKeyInfo = conversation.get('senderKeyInfo');
|
||||||
|
if (senderKeyInfo && senderKeyInfo.distributionId) {
|
||||||
|
const senderKeyDistributionMessage = await window.textsecure.messaging.getSenderKeyDistributionMessage(
|
||||||
|
senderKeyInfo.distributionId
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentProto: {
|
||||||
|
...contentProto,
|
||||||
|
senderKeyDistributionMessage: senderKeyDistributionMessage.serialize(),
|
||||||
|
},
|
||||||
|
groupId: conversation.get('groupId'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentProto,
|
||||||
|
groupId: conversation.get('groupId'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestResend(decryptionError: DecryptionErrorEventData) {
|
||||||
|
const {
|
||||||
|
cipherTextBytes,
|
||||||
|
cipherTextType,
|
||||||
|
contentHint,
|
||||||
|
groupId,
|
||||||
|
receivedAtCounter,
|
||||||
|
receivedAtDate,
|
||||||
|
senderDevice,
|
||||||
|
senderUuid,
|
||||||
|
timestamp,
|
||||||
|
} = decryptionError;
|
||||||
|
const logId = `${senderUuid}.${senderDevice} ${timestamp}`;
|
||||||
|
|
||||||
|
window.log.info(`requestResend/${logId}: Starting...`, {
|
||||||
|
cipherTextBytesLength: cipherTextBytes?.byteLength,
|
||||||
|
cipherTextType,
|
||||||
|
contentHint,
|
||||||
|
groupId: groupId ? `groupv2(${groupId})` : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Find the target conversation
|
||||||
|
|
||||||
|
const group = groupId
|
||||||
|
? window.ConversationController.get(groupId)
|
||||||
|
: undefined;
|
||||||
|
const sender = window.ConversationController.getOrCreate(
|
||||||
|
senderUuid,
|
||||||
|
'private'
|
||||||
|
);
|
||||||
|
const conversation = group || sender;
|
||||||
|
|
||||||
|
// 2. Send resend request
|
||||||
|
|
||||||
|
if (!cipherTextBytes || !isNumber(cipherTextType)) {
|
||||||
|
window.log.warn(
|
||||||
|
`requestResend/${logId}: Missing cipherText information, failing over to automatic reset`
|
||||||
|
);
|
||||||
|
startAutomaticSessionReset(decryptionError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = DecryptionErrorMessage.forOriginal(
|
||||||
|
Buffer.from(cipherTextBytes),
|
||||||
|
cipherTextType,
|
||||||
|
timestamp,
|
||||||
|
senderDevice
|
||||||
|
);
|
||||||
|
|
||||||
|
const plaintext = PlaintextContent.from(message);
|
||||||
|
const options = await getSendOptions(conversation.attributes);
|
||||||
|
const result = await handleMessageSend(
|
||||||
|
window.textsecure.messaging.sendRetryRequest({
|
||||||
|
plaintext,
|
||||||
|
options,
|
||||||
|
groupId,
|
||||||
|
uuid: senderUuid,
|
||||||
|
}),
|
||||||
|
{ messageIds: [], sendType: 'retryRequest' }
|
||||||
|
);
|
||||||
|
if (result && result.errors && result.errors.length > 0) {
|
||||||
|
throw result.errors[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
`requestResend/${logId}: Failed to send retry request, failing over to automatic reset`,
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
startAutomaticSessionReset(decryptionError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
|
|
||||||
|
// 3. Determine how to represent this to the user. Three different options.
|
||||||
|
|
||||||
|
// We believe that it could be successfully re-sent, so we'll add a placeholder.
|
||||||
|
if (contentHint === ContentHint.RESENDABLE) {
|
||||||
|
const { retryPlaceholders } = window.Signal.Services;
|
||||||
|
assert(retryPlaceholders, 'requestResend: adding placeholder');
|
||||||
|
|
||||||
|
window.log.info(`requestResend/${logId}: Adding placeholder`);
|
||||||
|
|
||||||
|
const state = window.reduxStore.getState();
|
||||||
|
const selectedId = state.conversations.selectedConversationId;
|
||||||
|
const wasOpened = selectedId === conversation.id;
|
||||||
|
|
||||||
|
await retryPlaceholders.add({
|
||||||
|
conversationId: conversation.get('id'),
|
||||||
|
receivedAt: receivedAtDate,
|
||||||
|
receivedAtCounter,
|
||||||
|
sentAt: timestamp,
|
||||||
|
senderUuid,
|
||||||
|
wasOpened,
|
||||||
|
});
|
||||||
|
|
||||||
|
maybeShowDecryptionToast(logId);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This message cannot be resent. We'll show no error and trust the other side to
|
||||||
|
// reset their session.
|
||||||
|
if (contentHint === ContentHint.IMPLICIT) {
|
||||||
|
maybeShowDecryptionToast(logId);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.warn(
|
||||||
|
`requestResend/${logId}: No content hint, adding error immediately`
|
||||||
|
);
|
||||||
|
conversation.queueJob('addDeliveryIssue', async () => {
|
||||||
|
conversation.addDeliveryIssue({
|
||||||
|
receivedAt: receivedAtDate,
|
||||||
|
receivedAtCounter,
|
||||||
|
senderUuid,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSessionReset(senderUuid: string, senderDevice: number) {
|
||||||
|
// Postpone sending light session resets until the queue is empty
|
||||||
|
const { lightSessionResetQueue } = window.Signal.Services;
|
||||||
|
|
||||||
|
if (!lightSessionResetQueue) {
|
||||||
|
throw new Error(
|
||||||
|
'scheduleSessionReset: lightSessionResetQueue is not available!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lightSessionResetQueue.add(() => {
|
||||||
|
window.textsecure.storage.protocol.lightSessionReset(
|
||||||
|
senderUuid,
|
||||||
|
senderDevice
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAutomaticSessionReset(decryptionError: DecryptionErrorEventData) {
|
||||||
|
const { senderUuid, senderDevice, timestamp } = decryptionError;
|
||||||
|
const logId = `${senderUuid}.${senderDevice} ${timestamp}`;
|
||||||
|
|
||||||
|
window.log.info(`startAutomaticSessionReset/${logId}: Starting...`);
|
||||||
|
|
||||||
|
scheduleSessionReset(senderUuid, senderDevice);
|
||||||
|
|
||||||
|
const conversationId = window.ConversationController.ensureContactIds({
|
||||||
|
uuid: senderUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!conversationId) {
|
||||||
|
window.log.warn(
|
||||||
|
'onLightSessionReset: No conversation id, cannot add message to timeline'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
window.log.warn(
|
||||||
|
'onLightSessionReset: No conversation, cannot add message to timeline'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const receivedAt = Date.now();
|
||||||
|
const receivedAtCounter = window.Signal.Util.incrementMessageCounter();
|
||||||
|
conversation.queueJob('addChatSessionRefreshed', async () => {
|
||||||
|
conversation.addChatSessionRefreshed({ receivedAt, receivedAtCounter });
|
||||||
|
});
|
||||||
|
}
|
|
@ -14105,20 +14105,6 @@
|
||||||
"updated": "2021-01-21T23:06:13.270Z",
|
"updated": "2021-01-21T23:06:13.270Z",
|
||||||
"reasonDetail": "Doesn't manipulate the DOM."
|
"reasonDetail": "Doesn't manipulate the DOM."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "ts/shims/textsecure.js",
|
|
||||||
"line": " wrap(textsecure.messaging.sendStickerPackSync([",
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-02-07T19:52:28.522Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "ts/shims/textsecure.ts",
|
|
||||||
"line": " wrap(",
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-02-07T19:52:28.522Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
"path": "ts/types/Stickers.js",
|
"path": "ts/types/Stickers.js",
|
||||||
|
|
|
@ -3,8 +3,10 @@
|
||||||
|
|
||||||
import { ConversationAttributesType } from '../model-types.d';
|
import { ConversationAttributesType } from '../model-types.d';
|
||||||
import { handleMessageSend } from './handleMessageSend';
|
import { handleMessageSend } from './handleMessageSend';
|
||||||
|
import { getSendOptions } from './getSendOptions';
|
||||||
import { sendReadReceiptsFor } from './sendReadReceiptsFor';
|
import { sendReadReceiptsFor } from './sendReadReceiptsFor';
|
||||||
import { hasErrors } from '../state/selectors/message';
|
import { hasErrors } from '../state/selectors/message';
|
||||||
|
import { isNotNil } from './isNotNil';
|
||||||
|
|
||||||
export async function markConversationRead(
|
export async function markConversationRead(
|
||||||
conversationAttrs: ConversationAttributesType,
|
conversationAttrs: ConversationAttributesType,
|
||||||
|
@ -43,6 +45,7 @@ export async function markConversationRead(
|
||||||
const unreadReactionSyncData = new Map<
|
const unreadReactionSyncData = new Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
|
messageId?: string;
|
||||||
senderUuid?: string;
|
senderUuid?: string;
|
||||||
senderE164?: string;
|
senderE164?: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
@ -54,6 +57,7 @@ export async function markConversationRead(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unreadReactionSyncData.set(targetKey, {
|
unreadReactionSyncData.set(targetKey, {
|
||||||
|
messageId: reaction.messageId,
|
||||||
senderE164: undefined,
|
senderE164: undefined,
|
||||||
senderUuid: reaction.targetAuthorUuid,
|
senderUuid: reaction.targetAuthorUuid,
|
||||||
timestamp: reaction.targetTimestamp,
|
timestamp: reaction.targetTimestamp,
|
||||||
|
@ -68,6 +72,7 @@ export async function markConversationRead(
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
messageId: messageSyncData.id,
|
||||||
senderE164: messageSyncData.source,
|
senderE164: messageSyncData.source,
|
||||||
senderUuid: messageSyncData.sourceUuid,
|
senderUuid: messageSyncData.sourceUuid,
|
||||||
senderId: window.ConversationController.ensureContactIds({
|
senderId: window.ConversationController.ensureContactIds({
|
||||||
|
@ -89,25 +94,39 @@ export async function markConversationRead(
|
||||||
item => Boolean(item.senderId) && !item.hasErrors
|
item => Boolean(item.senderId) && !item.hasErrors
|
||||||
);
|
);
|
||||||
|
|
||||||
const readSyncs = [
|
const readSyncs: Array<{
|
||||||
|
messageId?: string;
|
||||||
|
senderE164?: string;
|
||||||
|
senderUuid?: string;
|
||||||
|
senderId?: string;
|
||||||
|
timestamp: number;
|
||||||
|
hasErrors?: string;
|
||||||
|
}> = [
|
||||||
...unreadMessagesSyncData,
|
...unreadMessagesSyncData,
|
||||||
...Array.from(unreadReactionSyncData.values()),
|
...Array.from(unreadReactionSyncData.values()),
|
||||||
];
|
];
|
||||||
|
const messageIds = readSyncs.map(item => item.messageId).filter(isNotNil);
|
||||||
|
|
||||||
if (readSyncs.length && options.sendReadReceipts) {
|
if (readSyncs.length && options.sendReadReceipts) {
|
||||||
window.log.info(`Sending ${readSyncs.length} read syncs`);
|
window.log.info(`Sending ${readSyncs.length} read syncs`);
|
||||||
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
|
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
|
||||||
// to a contact, we need accessKeys for both.
|
// to a contact, we need accessKeys for both.
|
||||||
const {
|
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||||
sendOptions,
|
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||||
} = await window.ConversationController.prepareForSend(
|
syncMessage: true,
|
||||||
window.ConversationController.getOurConversationId(),
|
});
|
||||||
{ syncMessage: true }
|
|
||||||
);
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
window.log.warn(
|
||||||
|
'markConversationRead: We are primary device; not sending read syncs'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await handleMessageSend(
|
||||||
|
window.textsecure.messaging.syncReadMessages(readSyncs, sendOptions),
|
||||||
|
{ messageIds, sendType: 'readSync' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await handleMessageSend(
|
|
||||||
window.textsecure.messaging.syncReadMessages(readSyncs, sendOptions)
|
|
||||||
);
|
|
||||||
await sendReadReceiptsFor(conversationAttrs, unreadMessagesSyncData);
|
await sendReadReceiptsFor(conversationAttrs, unreadMessagesSyncData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,14 +63,14 @@ export class RetryPlaceholders {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.items = parsed.success ? parsed.data : [];
|
this.items = parsed.success ? parsed.data : [];
|
||||||
window.log.info(
|
|
||||||
`RetryPlaceholders.constructor: Started with ${this.items.length} items`
|
|
||||||
);
|
|
||||||
|
|
||||||
this.sortByExpiresAtAsc();
|
this.sortByExpiresAtAsc();
|
||||||
this.byConversation = this.makeByConversationLookup();
|
this.byConversation = this.makeByConversationLookup();
|
||||||
this.byMessage = this.makeByMessageLookup();
|
this.byMessage = this.makeByMessageLookup();
|
||||||
this.retryReceiptLifespan = options.retryReceiptLifespan || HOUR;
|
this.retryReceiptLifespan = options.retryReceiptLifespan || HOUR;
|
||||||
|
|
||||||
|
window.log.info(
|
||||||
|
`RetryPlaceholders.constructor: Started with ${this.items.length} items, lifespan of ${this.retryReceiptLifespan}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arranging local data for efficiency
|
// Arranging local data for efficiency
|
||||||
|
|
|
@ -7,9 +7,18 @@ import { getSendOptions } from './getSendOptions';
|
||||||
import { handleMessageSend } from './handleMessageSend';
|
import { handleMessageSend } from './handleMessageSend';
|
||||||
import { isConversationAccepted } from './isConversationAccepted';
|
import { isConversationAccepted } from './isConversationAccepted';
|
||||||
|
|
||||||
|
type ReceiptSpecType = {
|
||||||
|
messageId: string;
|
||||||
|
senderE164?: string;
|
||||||
|
senderUuid?: string;
|
||||||
|
senderId?: string;
|
||||||
|
timestamp: number;
|
||||||
|
hasErrors: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export async function sendReadReceiptsFor(
|
export async function sendReadReceiptsFor(
|
||||||
conversationAttrs: ConversationAttributesType,
|
conversationAttrs: ConversationAttributesType,
|
||||||
items: Array<unknown>
|
items: Array<ReceiptSpecType>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Only send read receipts for accepted conversations
|
// Only send read receipts for accepted conversations
|
||||||
if (
|
if (
|
||||||
|
@ -22,7 +31,8 @@ export async function sendReadReceiptsFor(
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
map(receiptsBySender, async (receipts, senderId) => {
|
map(receiptsBySender, async (receipts, senderId) => {
|
||||||
const timestamps = map(receipts, 'timestamp');
|
const timestamps = map(receipts, item => item.timestamp);
|
||||||
|
const messageIds = map(receipts, item => item.messageId);
|
||||||
const conversation = window.ConversationController.get(senderId);
|
const conversation = window.ConversationController.get(senderId);
|
||||||
|
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
|
@ -34,7 +44,8 @@ export async function sendReadReceiptsFor(
|
||||||
senderUuid: conversation.get('uuid')!,
|
senderUuid: conversation.get('uuid')!,
|
||||||
timestamps,
|
timestamps,
|
||||||
options: sendOptions,
|
options: sendOptions,
|
||||||
})
|
}),
|
||||||
|
{ messageIds, sendType: 'readReceipt' }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { differenceWith, partition } from 'lodash';
|
import { differenceWith, omit, partition } from 'lodash';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -16,6 +16,7 @@ import { senderCertificateService } from '../services/senderCertificate';
|
||||||
import {
|
import {
|
||||||
padMessage,
|
padMessage,
|
||||||
SenderCertificateMode,
|
SenderCertificateMode,
|
||||||
|
SendLogCallbackType,
|
||||||
} from '../textsecure/OutgoingMessage';
|
} from '../textsecure/OutgoingMessage';
|
||||||
import { isEnabled } from '../RemoteConfig';
|
import { isEnabled } from '../RemoteConfig';
|
||||||
|
|
||||||
|
@ -30,7 +31,12 @@ import { ConversationModel } from '../models/conversations';
|
||||||
import { DeviceType } from '../textsecure/Types.d';
|
import { DeviceType } from '../textsecure/Types.d';
|
||||||
import { getKeysForIdentifier } from '../textsecure/getKeysForIdentifier';
|
import { getKeysForIdentifier } from '../textsecure/getKeysForIdentifier';
|
||||||
import { ConversationAttributesType } from '../model-types.d';
|
import { ConversationAttributesType } from '../model-types.d';
|
||||||
import { SEALED_SENDER } from './handleMessageSend';
|
import {
|
||||||
|
handleMessageSend,
|
||||||
|
SEALED_SENDER,
|
||||||
|
SendTypesType,
|
||||||
|
shouldSaveProto,
|
||||||
|
} from './handleMessageSend';
|
||||||
import { parseIntOrThrow } from './parseIntOrThrow';
|
import { parseIntOrThrow } from './parseIntOrThrow';
|
||||||
import {
|
import {
|
||||||
multiRecipient200ResponseSchema,
|
multiRecipient200ResponseSchema,
|
||||||
|
@ -59,17 +65,21 @@ const FIXMEU8 = Uint8Array;
|
||||||
// Public API:
|
// Public API:
|
||||||
|
|
||||||
export async function sendToGroup({
|
export async function sendToGroup({
|
||||||
groupSendOptions,
|
|
||||||
conversation,
|
|
||||||
contentHint,
|
contentHint,
|
||||||
sendOptions,
|
conversation,
|
||||||
|
groupSendOptions,
|
||||||
|
messageId,
|
||||||
isPartialSend,
|
isPartialSend,
|
||||||
|
sendOptions,
|
||||||
|
sendType,
|
||||||
}: {
|
}: {
|
||||||
groupSendOptions: GroupSendOptionsType;
|
|
||||||
conversation: ConversationModel;
|
|
||||||
contentHint: number;
|
contentHint: number;
|
||||||
sendOptions?: SendOptionsType;
|
conversation: ConversationModel;
|
||||||
|
groupSendOptions: GroupSendOptionsType;
|
||||||
isPartialSend?: boolean;
|
isPartialSend?: boolean;
|
||||||
|
messageId: string | undefined;
|
||||||
|
sendOptions?: SendOptionsType;
|
||||||
|
sendType: SendTypesType;
|
||||||
}): Promise<CallbackResultType> {
|
}): Promise<CallbackResultType> {
|
||||||
assert(
|
assert(
|
||||||
window.textsecure.messaging,
|
window.textsecure.messaging,
|
||||||
|
@ -92,8 +102,10 @@ export async function sendToGroup({
|
||||||
contentMessage,
|
contentMessage,
|
||||||
conversation,
|
conversation,
|
||||||
isPartialSend,
|
isPartialSend,
|
||||||
|
messageId,
|
||||||
recipients,
|
recipients,
|
||||||
sendOptions,
|
sendOptions,
|
||||||
|
sendType,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -103,18 +115,22 @@ export async function sendContentMessageToGroup({
|
||||||
contentMessage,
|
contentMessage,
|
||||||
conversation,
|
conversation,
|
||||||
isPartialSend,
|
isPartialSend,
|
||||||
|
messageId,
|
||||||
online,
|
online,
|
||||||
recipients,
|
recipients,
|
||||||
sendOptions,
|
sendOptions,
|
||||||
|
sendType,
|
||||||
timestamp,
|
timestamp,
|
||||||
}: {
|
}: {
|
||||||
contentHint: number;
|
contentHint: number;
|
||||||
contentMessage: Proto.Content;
|
contentMessage: Proto.Content;
|
||||||
conversation: ConversationModel;
|
conversation: ConversationModel;
|
||||||
isPartialSend?: boolean;
|
isPartialSend?: boolean;
|
||||||
|
messageId: string | undefined;
|
||||||
online?: boolean;
|
online?: boolean;
|
||||||
recipients: Array<string>;
|
recipients: Array<string>;
|
||||||
sendOptions?: SendOptionsType;
|
sendOptions?: SendOptionsType;
|
||||||
|
sendType: SendTypesType;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}): Promise<CallbackResultType> {
|
}): Promise<CallbackResultType> {
|
||||||
const logId = conversation.idForLogging();
|
const logId = conversation.idForLogging();
|
||||||
|
@ -127,7 +143,7 @@ export async function sendContentMessageToGroup({
|
||||||
const ourConversation = window.ConversationController.get(ourConversationId);
|
const ourConversation = window.ConversationController.get(ourConversationId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isEnabled('desktop.sendSenderKey') &&
|
isEnabled('desktop.sendSenderKey2') &&
|
||||||
ourConversation?.get('capabilities')?.senderKey &&
|
ourConversation?.get('capabilities')?.senderKey &&
|
||||||
isGroupV2(conversation.attributes)
|
isGroupV2(conversation.attributes)
|
||||||
) {
|
) {
|
||||||
|
@ -137,10 +153,12 @@ export async function sendContentMessageToGroup({
|
||||||
contentMessage,
|
contentMessage,
|
||||||
conversation,
|
conversation,
|
||||||
isPartialSend,
|
isPartialSend,
|
||||||
|
messageId,
|
||||||
online,
|
online,
|
||||||
recipients,
|
recipients,
|
||||||
recursionCount: 0,
|
recursionCount: 0,
|
||||||
sendOptions,
|
sendOptions,
|
||||||
|
sendType,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -151,16 +169,24 @@ export async function sendContentMessageToGroup({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sendLogCallback = window.textsecure.messaging.makeSendLogCallback({
|
||||||
|
contentHint,
|
||||||
|
messageId,
|
||||||
|
proto: Buffer.from(Proto.Content.encode(contentMessage).finish()),
|
||||||
|
sendType,
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
const groupId = isGroupV2(conversation.attributes)
|
const groupId = isGroupV2(conversation.attributes)
|
||||||
? conversation.get('groupId')
|
? conversation.get('groupId')
|
||||||
: undefined;
|
: undefined;
|
||||||
return window.textsecure.messaging.sendGroupProto({
|
return window.textsecure.messaging.sendGroupProto({
|
||||||
recipients,
|
|
||||||
proto: contentMessage,
|
|
||||||
timestamp,
|
|
||||||
contentHint,
|
contentHint,
|
||||||
groupId,
|
groupId,
|
||||||
options: { ...sendOptions, online },
|
options: { ...sendOptions, online },
|
||||||
|
proto: contentMessage,
|
||||||
|
recipients,
|
||||||
|
sendLogCallback,
|
||||||
|
timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,10 +197,12 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
contentMessage: Proto.Content;
|
contentMessage: Proto.Content;
|
||||||
conversation: ConversationModel;
|
conversation: ConversationModel;
|
||||||
isPartialSend?: boolean;
|
isPartialSend?: boolean;
|
||||||
|
messageId: string | undefined;
|
||||||
online?: boolean;
|
online?: boolean;
|
||||||
recipients: Array<string>;
|
recipients: Array<string>;
|
||||||
recursionCount: number;
|
recursionCount: number;
|
||||||
sendOptions?: SendOptionsType;
|
sendOptions?: SendOptionsType;
|
||||||
|
sendType: SendTypesType;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}): Promise<CallbackResultType> {
|
}): Promise<CallbackResultType> {
|
||||||
const {
|
const {
|
||||||
|
@ -182,10 +210,12 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
contentMessage,
|
contentMessage,
|
||||||
conversation,
|
conversation,
|
||||||
isPartialSend,
|
isPartialSend,
|
||||||
|
messageId,
|
||||||
online,
|
online,
|
||||||
recursionCount,
|
recursionCount,
|
||||||
recipients,
|
recipients,
|
||||||
sendOptions,
|
sendOptions,
|
||||||
|
sendType,
|
||||||
timestamp,
|
timestamp,
|
||||||
} = options;
|
} = options;
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
|
@ -287,12 +317,16 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
currentDevices,
|
currentDevices,
|
||||||
device => isValidSenderKeyRecipient(conversation, device.identifier)
|
device => isValidSenderKeyRecipient(conversation, device.identifier)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const senderKeyRecipients = getUuidsFromDevices(devicesForSenderKey);
|
||||||
|
const normalSendRecipients = getUuidsFromDevices(devicesForNormalSend);
|
||||||
window.log.info(
|
window.log.info(
|
||||||
`sendToGroupViaSenderKey/${logId}: ${devicesForSenderKey.length} devices for sender key, ${devicesForNormalSend.length} devices for normal send`
|
`sendToGroupViaSenderKey/${logId}:` +
|
||||||
|
` ${senderKeyRecipients.length} accounts for sender key (${devicesForSenderKey.length} devices),` +
|
||||||
|
` ${normalSendRecipients.length} accounts for normal send (${devicesForNormalSend.length} devices)`
|
||||||
);
|
);
|
||||||
|
|
||||||
// 5. Ensure we have enough recipients
|
// 5. Ensure we have enough recipients
|
||||||
const senderKeyRecipients = getUuidsFromDevices(devicesForSenderKey);
|
|
||||||
if (senderKeyRecipients.length < 2) {
|
if (senderKeyRecipients.length < 2) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`sendToGroupViaSenderKey/${logId}: Not enough recipients for Sender Key message. Failing over.`
|
`sendToGroupViaSenderKey/${logId}: Not enough recipients for Sender Key message. Failing over.`
|
||||||
|
@ -335,14 +369,17 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
newToMemberUuids.length
|
newToMemberUuids.length
|
||||||
} members: ${JSON.stringify(newToMemberUuids)}`
|
} members: ${JSON.stringify(newToMemberUuids)}`
|
||||||
);
|
);
|
||||||
await window.textsecure.messaging.sendSenderKeyDistributionMessage(
|
await handleMessageSend(
|
||||||
{
|
window.textsecure.messaging.sendSenderKeyDistributionMessage(
|
||||||
contentHint: ContentHint.DEFAULT,
|
{
|
||||||
distributionId,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
groupId,
|
distributionId,
|
||||||
identifiers: newToMemberUuids,
|
groupId,
|
||||||
},
|
identifiers: newToMemberUuids,
|
||||||
sendOptions
|
},
|
||||||
|
sendOptions
|
||||||
|
),
|
||||||
|
{ messageIds: [], sendType: 'senderKeyDistributionMessage' }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -368,6 +405,14 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Send the Sender Key message!
|
// 10. Send the Sender Key message!
|
||||||
|
let sendLogId: number;
|
||||||
|
let senderKeyRecipientsWithDevices: Record<string, Array<number>> = {};
|
||||||
|
devicesForSenderKey.forEach(item => {
|
||||||
|
const { id, identifier } = item;
|
||||||
|
senderKeyRecipientsWithDevices[identifier] ||= [];
|
||||||
|
senderKeyRecipientsWithDevices[identifier].push(id);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const messageBuffer = await encryptForSenderKey({
|
const messageBuffer = await encryptForSenderKey({
|
||||||
contentHint,
|
contentHint,
|
||||||
|
@ -397,6 +442,11 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
senderKeyRecipientsWithDevices = omit(
|
||||||
|
senderKeyRecipientsWithDevices,
|
||||||
|
uuids404 || []
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
`sendToGroupViaSenderKey/${logId}: Server returned unexpected 200 response ${JSON.stringify(
|
`sendToGroupViaSenderKey/${logId}: Server returned unexpected 200 response ${JSON.stringify(
|
||||||
|
@ -404,6 +454,20 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldSaveProto(sendType)) {
|
||||||
|
sendLogId = await window.Signal.Data.insertSentProto(
|
||||||
|
{
|
||||||
|
contentHint,
|
||||||
|
proto: Buffer.from(Proto.Content.encode(contentMessage).finish()),
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recipients: senderKeyRecipientsWithDevices,
|
||||||
|
messageIds: messageId ? [messageId] : [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === ERROR_EXPIRED_OR_MISSING_DEVICES) {
|
if (error.code === ERROR_EXPIRED_OR_MISSING_DEVICES) {
|
||||||
await handle409Response(logId, error);
|
await handle409Response(logId, error);
|
||||||
|
@ -426,13 +490,14 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`sendToGroupViaSenderKey/${logId}: Returned unexpected error ${error.code}. Failing over.`
|
`sendToGroupViaSenderKey/${logId}: Returned unexpected error ${
|
||||||
|
error.code
|
||||||
|
}. Failing over. ${error.stack || error}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 11. Return early if there are no normal send recipients
|
// 11. Return early if there are no normal send recipients
|
||||||
const normalRecipients = getUuidsFromDevices(devicesForNormalSend);
|
if (normalSendRecipients.length === 0) {
|
||||||
if (normalRecipients.length === 0) {
|
|
||||||
return {
|
return {
|
||||||
dataMessage: contentMessage.dataMessage
|
dataMessage: contentMessage.dataMessage
|
||||||
? toArrayBuffer(
|
? toArrayBuffer(
|
||||||
|
@ -441,18 +506,59 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
: undefined,
|
: undefined,
|
||||||
successfulIdentifiers: senderKeyRecipients,
|
successfulIdentifiers: senderKeyRecipients,
|
||||||
unidentifiedDeliveries: senderKeyRecipients,
|
unidentifiedDeliveries: senderKeyRecipients,
|
||||||
|
|
||||||
|
contentHint,
|
||||||
|
timestamp,
|
||||||
|
contentProto: Buffer.from(Proto.Content.encode(contentMessage).finish()),
|
||||||
|
recipients: senderKeyRecipientsWithDevices,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 12. Send normal message to the leftover normal recipients. Then combine normal send
|
// 12. Send normal message to the leftover normal recipients. Then combine normal send
|
||||||
// result with result from sender key send for final return value.
|
// result with result from sender key send for final return value.
|
||||||
|
|
||||||
|
// We don't want to use a normal send log callback here, because the proto has already
|
||||||
|
// been saved as part of the Sender Key send. We're just adding recipients here.
|
||||||
|
const sendLogCallback: SendLogCallbackType = async ({
|
||||||
|
identifier,
|
||||||
|
deviceIds,
|
||||||
|
}: {
|
||||||
|
identifier: string;
|
||||||
|
deviceIds: Array<number>;
|
||||||
|
}) => {
|
||||||
|
if (!shouldSaveProto(sendType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentToConversation = window.ConversationController.get(identifier);
|
||||||
|
if (!sentToConversation) {
|
||||||
|
window.log.warn(
|
||||||
|
`sendToGroupViaSenderKey/callback: Unable to find conversation for identifier ${identifier}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const recipientUuid = sentToConversation.get('uuid');
|
||||||
|
if (!recipientUuid) {
|
||||||
|
window.log.warn(
|
||||||
|
`sendToGroupViaSenderKey/callback: Conversation ${conversation.idForLogging()} had no UUID`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.Signal.Data.insertProtoRecipients({
|
||||||
|
id: sendLogId,
|
||||||
|
recipientUuid,
|
||||||
|
deviceIds,
|
||||||
|
});
|
||||||
|
};
|
||||||
const normalSendResult = await window.textsecure.messaging.sendGroupProto({
|
const normalSendResult = await window.textsecure.messaging.sendGroupProto({
|
||||||
recipients: normalRecipients,
|
|
||||||
proto: contentMessage,
|
|
||||||
timestamp,
|
|
||||||
contentHint,
|
contentHint,
|
||||||
groupId,
|
groupId,
|
||||||
options: { ...sendOptions, online },
|
options: { ...sendOptions, online },
|
||||||
|
proto: contentMessage,
|
||||||
|
recipients: normalSendRecipients,
|
||||||
|
sendLogCallback,
|
||||||
|
timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -471,6 +577,14 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
...(normalSendResult.unidentifiedDeliveries || []),
|
...(normalSendResult.unidentifiedDeliveries || []),
|
||||||
...senderKeyRecipients,
|
...senderKeyRecipients,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
contentHint,
|
||||||
|
timestamp,
|
||||||
|
contentProto: Buffer.from(Proto.Content.encode(contentMessage).finish()),
|
||||||
|
recipients: {
|
||||||
|
...normalSendResult.recipients,
|
||||||
|
...senderKeyRecipientsWithDevices,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -202,6 +202,24 @@ Whisper.TapToViewExpiredOutgoingToast = Whisper.ToastView.extend({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Whisper.DecryptionErrorToast = Whisper.ToastView.extend({
|
||||||
|
className: 'toast toast-clickable',
|
||||||
|
initialize() {
|
||||||
|
this.timeout = 10000;
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
click: 'onClick',
|
||||||
|
},
|
||||||
|
render_attributes() {
|
||||||
|
return {
|
||||||
|
toastMessage: window.i18n('decryptionErrorToast'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onClick() {
|
||||||
|
window.showDebugLog();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Whisper.FileSavedToast = Whisper.ToastView.extend({
|
Whisper.FileSavedToast = Whisper.ToastView.extend({
|
||||||
className: 'toast toast-clickable',
|
className: 'toast toast-clickable',
|
||||||
initialize(options: any) {
|
initialize(options: any) {
|
||||||
|
@ -2939,7 +2957,10 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
okText: window.i18n('delete'),
|
okText: window.i18n('delete'),
|
||||||
resolve: async () => {
|
resolve: async () => {
|
||||||
try {
|
try {
|
||||||
await this.model.sendDeleteForEveryoneMessage(message.get('sent_at'));
|
await this.model.sendDeleteForEveryoneMessage({
|
||||||
|
id: message.id,
|
||||||
|
timestamp: message.get('sent_at'),
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'Error sending delete-for-everyone',
|
'Error sending delete-for-everyone',
|
||||||
|
@ -3673,6 +3694,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.model.sendReactionMessage(reaction, {
|
await this.model.sendReactionMessage(reaction, {
|
||||||
|
messageId,
|
||||||
targetAuthorUuid: messageModel.getSourceUuid(),
|
targetAuthorUuid: messageModel.getSourceUuid(),
|
||||||
targetTimestamp: messageModel.get('sent_at'),
|
targetTimestamp: messageModel.get('sent_at'),
|
||||||
});
|
});
|
||||||
|
|
20
ts/window.d.ts
vendored
20
ts/window.d.ts
vendored
|
@ -16,7 +16,6 @@ import {
|
||||||
MessageModelCollectionType,
|
MessageModelCollectionType,
|
||||||
MessageAttributesType,
|
MessageAttributesType,
|
||||||
ReactionAttributesType,
|
ReactionAttributesType,
|
||||||
ReactionModelType,
|
|
||||||
} from './model-types.d';
|
} from './model-types.d';
|
||||||
import { TextSecureType } from './textsecure.d';
|
import { TextSecureType } from './textsecure.d';
|
||||||
import { Storage } from './textsecure/Storage';
|
import { Storage } from './textsecure/Storage';
|
||||||
|
@ -241,6 +240,7 @@ declare global {
|
||||||
showWindow: () => void;
|
showWindow: () => void;
|
||||||
showSettings: () => void;
|
showSettings: () => void;
|
||||||
shutdown: () => void;
|
shutdown: () => void;
|
||||||
|
showDebugLog: () => void;
|
||||||
sendChallengeRequest: (request: IPCChallengeRequest) => void;
|
sendChallengeRequest: (request: IPCChallengeRequest) => void;
|
||||||
setAutoHideMenuBar: (value: WhatIsThis) => void;
|
setAutoHideMenuBar: (value: WhatIsThis) => void;
|
||||||
setBadgeCount: (count: number) => void;
|
setBadgeCount: (count: number) => void;
|
||||||
|
@ -290,6 +290,7 @@ declare global {
|
||||||
onTimeout: (timestamp: number, cb: () => void, id?: string) => string;
|
onTimeout: (timestamp: number, cb: () => void, id?: string) => string;
|
||||||
removeTimeout: (uuid: string) => void;
|
removeTimeout: (uuid: string) => void;
|
||||||
retryPlaceholders?: Util.RetryPlaceholders;
|
retryPlaceholders?: Util.RetryPlaceholders;
|
||||||
|
lightSessionResetQueue?: PQueue;
|
||||||
runStorageServiceSyncJob: () => Promise<void>;
|
runStorageServiceSyncJob: () => Promise<void>;
|
||||||
storageServiceUploadJob: () => void;
|
storageServiceUploadJob: () => void;
|
||||||
};
|
};
|
||||||
|
@ -494,6 +495,7 @@ declare global {
|
||||||
GV2_ENABLE_STATE_PROCESSING: boolean;
|
GV2_ENABLE_STATE_PROCESSING: boolean;
|
||||||
GV2_MIGRATION_DISABLE_ADD: boolean;
|
GV2_MIGRATION_DISABLE_ADD: boolean;
|
||||||
GV2_MIGRATION_DISABLE_INVITE: boolean;
|
GV2_MIGRATION_DISABLE_INVITE: boolean;
|
||||||
|
RETRY_DELAY: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to extend `Error`, so we need an interface.
|
// We want to extend `Error`, so we need an interface.
|
||||||
|
@ -536,6 +538,13 @@ export class CanvasVideoRenderer {
|
||||||
constructor(canvas: Ref<HTMLCanvasElement>);
|
constructor(canvas: Ref<HTMLCanvasElement>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeliveryReceiptBatcherItemType = {
|
||||||
|
messageId: string;
|
||||||
|
source?: string;
|
||||||
|
sourceUuid?: string;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type LoggerType = {
|
export type LoggerType = {
|
||||||
fatal: LogFunctionType;
|
fatal: LogFunctionType;
|
||||||
info: LogFunctionType;
|
info: LogFunctionType;
|
||||||
|
@ -614,12 +623,8 @@ export type WhisperType = {
|
||||||
ExpiringMessagesListener: WhatIsThis;
|
ExpiringMessagesListener: WhatIsThis;
|
||||||
TapToViewMessagesListener: WhatIsThis;
|
TapToViewMessagesListener: WhatIsThis;
|
||||||
|
|
||||||
deliveryReceiptQueue: PQueue<WhatIsThis>;
|
deliveryReceiptQueue: PQueue;
|
||||||
deliveryReceiptBatcher: BatcherType<{
|
deliveryReceiptBatcher: BatcherType<DeliveryReceiptBatcherItemType>;
|
||||||
source?: string;
|
|
||||||
sourceUuid?: string;
|
|
||||||
timestamp: number;
|
|
||||||
}>;
|
|
||||||
RotateSignedPreKeyListener: WhatIsThis;
|
RotateSignedPreKeyListener: WhatIsThis;
|
||||||
|
|
||||||
AlreadyGroupMemberToast: typeof window.Whisper.ToastView;
|
AlreadyGroupMemberToast: typeof window.Whisper.ToastView;
|
||||||
|
@ -630,6 +635,7 @@ export type WhisperType = {
|
||||||
CaptchaSolvedToast: typeof window.Whisper.ToastView;
|
CaptchaSolvedToast: typeof window.Whisper.ToastView;
|
||||||
CaptchaFailedToast: typeof window.Whisper.ToastView;
|
CaptchaFailedToast: typeof window.Whisper.ToastView;
|
||||||
DangerousFileTypeToast: typeof window.Whisper.ToastView;
|
DangerousFileTypeToast: typeof window.Whisper.ToastView;
|
||||||
|
DecryptionErrorToast: typeof window.Whisper.ToastView;
|
||||||
ExpiredToast: typeof window.Whisper.ToastView;
|
ExpiredToast: typeof window.Whisper.ToastView;
|
||||||
FileSavedToast: typeof window.Whisper.ToastView;
|
FileSavedToast: typeof window.Whisper.ToastView;
|
||||||
FileSizeToast: any;
|
FileSizeToast: any;
|
||||||
|
|
Loading…
Reference in a new issue