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": {
|
||||
"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"
|
||||
|
|
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_INVITE = false;
|
||||
|
||||
window.RETRY_DELAY = false;
|
||||
|
||||
window.platform = process.platform;
|
||||
window.getTitle = () => title;
|
||||
window.getLocale = () => config.locale;
|
||||
|
@ -156,6 +158,10 @@ try {
|
|||
window.log.info('shutdown');
|
||||
ipc.send('shutdown');
|
||||
};
|
||||
window.showDebugLog = () => {
|
||||
window.log.info('showDebugLog');
|
||||
ipc.send('show-debug-log');
|
||||
};
|
||||
|
||||
window.closeAbout = () => ipc.send('close-about');
|
||||
window.readyForUpdates = () => ipc.send('ready-for-updates');
|
||||
|
|
|
@ -9,16 +9,12 @@ import {
|
|||
ConversationModelCollectionType,
|
||||
ConversationAttributesTypeType,
|
||||
} from './model-types.d';
|
||||
import { SendOptionsType, CallbackResultType } from './textsecure/SendMessage';
|
||||
import { ConversationModel } from './models/conversations';
|
||||
import { maybeDeriveGroupV2Id } from './groups';
|
||||
import { assert } from './util/assert';
|
||||
import { isValidGuid } from './util/isValidGuid';
|
||||
import { map, reduce } from './util/iterables';
|
||||
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;
|
||||
|
||||
|
@ -313,6 +309,25 @@ export class ConversationController {
|
|||
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
|
||||
* database id of the given contact. In high trust mode, it may create new contacts,
|
||||
|
@ -730,25 +745,6 @@ export class ConversationController {
|
|||
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(
|
||||
conversationId: string
|
||||
): Promise<Array<ConversationModel>> {
|
||||
|
|
|
@ -9,18 +9,19 @@ export type ConfigKeyType =
|
|||
| 'desktop.disableGV1'
|
||||
| 'desktop.groupCalling'
|
||||
| 'desktop.gv2'
|
||||
| 'desktop.internalUser'
|
||||
| 'desktop.mandatoryProfileSharing'
|
||||
| 'desktop.mediaQuality.levels'
|
||||
| 'desktop.messageRequests'
|
||||
| 'desktop.retryReceiptLifespan'
|
||||
| 'desktop.retryRespondMaxAge'
|
||||
| 'desktop.screensharing2'
|
||||
| 'desktop.sendSenderKey'
|
||||
| 'desktop.sendSenderKey2'
|
||||
| 'desktop.storage'
|
||||
| 'desktop.storageWrite3'
|
||||
| 'desktop.worksAtSignal'
|
||||
| 'global.groupsv2.maxGroupSize'
|
||||
| 'global.groupsv2.groupSizeHardLimit';
|
||||
| 'global.groupsv2.groupSizeHardLimit'
|
||||
| 'global.groupsv2.maxGroupSize';
|
||||
type ConfigValueType = {
|
||||
name: ConfigKeyType;
|
||||
enabled: boolean;
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
typedArrayToArrayBuffer,
|
||||
} from './Crypto';
|
||||
import { assert } from './util/assert';
|
||||
import { handleMessageSend } from './util/handleMessageSend';
|
||||
import { isNotNil } from './util/isNotNil';
|
||||
import { Zone } from './util/Zone';
|
||||
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
|
||||
|
||||
async enqueueSessionJob<T>(
|
||||
|
@ -1231,7 +1239,14 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
|
||||
// Send a null message with newly-created session
|
||||
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) {
|
||||
// If we failed to do the session reset, then we'll allow another attempt sooner
|
||||
// than one hour from now.
|
||||
|
|
603
ts/background.ts
603
ts/background.ts
|
@ -4,10 +4,6 @@
|
|||
import { isNumber, noop } from 'lodash';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { render } from 'react-dom';
|
||||
import {
|
||||
DecryptionErrorMessage,
|
||||
PlaintextContent,
|
||||
} from '@signalapp/signal-client';
|
||||
|
||||
import MessageReceiver from './textsecure/MessageReceiver';
|
||||
import { SessionResetsType, ProcessedDataMessage } from './textsecure/Types.d';
|
||||
|
@ -17,7 +13,7 @@ import {
|
|||
} from './model-types.d';
|
||||
import * as Bytes from './Bytes';
|
||||
import { typedArrayToArrayBuffer } from './Crypto';
|
||||
import { WhatIsThis } from './window.d';
|
||||
import { WhatIsThis, DeliveryReceiptBatcherItemType } from './window.d';
|
||||
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
|
||||
import { SocketStatus } from './types/SocketStatus';
|
||||
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
|
||||
|
@ -46,15 +42,11 @@ import {
|
|||
TypingEvent,
|
||||
ErrorEvent,
|
||||
DeliveryEvent,
|
||||
DecryptionErrorEvent,
|
||||
DecryptionErrorEventData,
|
||||
SentEvent,
|
||||
SentEventData,
|
||||
ProfileKeyUpdateEvent,
|
||||
MessageEvent,
|
||||
MessageEventData,
|
||||
RetryRequestEvent,
|
||||
RetryRequestEventData,
|
||||
ReadEvent,
|
||||
ConfigurationEvent,
|
||||
ViewSyncEvent,
|
||||
|
@ -72,6 +64,7 @@ import * as universalExpireTimer from './util/universalExpireTimer';
|
|||
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
|
||||
import { getSendOptions } from './util/getSendOptions';
|
||||
import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff';
|
||||
import { handleMessageSend } from './util/handleMessageSend';
|
||||
import { AppViewType } from './state/ducks/app';
|
||||
import { isIncoming } from './state/selectors/message';
|
||||
import { actionCreators } from './state/actions';
|
||||
|
@ -89,6 +82,7 @@ import {
|
|||
} from './types/SystemTraySetting';
|
||||
import * as Stickers from './types/Stickers';
|
||||
import { SignalService as Proto } from './protobuf';
|
||||
import { onRetryRequest, onDecryptionError } from './util/handleRetry';
|
||||
|
||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||
|
||||
|
@ -167,6 +161,7 @@ export async function startApp(): Promise<void> {
|
|||
profileKeyResponseQueue.pause();
|
||||
|
||||
const lightSessionResetQueue = new window.PQueue();
|
||||
window.Signal.Services.lightSessionResetQueue = lightSessionResetQueue;
|
||||
lightSessionResetQueue.pause();
|
||||
|
||||
window.Whisper.deliveryReceiptQueue = new window.PQueue({
|
||||
|
@ -174,57 +169,63 @@ export async function startApp(): Promise<void> {
|
|||
timeout: 1000 * 60 * 2,
|
||||
});
|
||||
window.Whisper.deliveryReceiptQueue.pause();
|
||||
window.Whisper.deliveryReceiptBatcher = window.Signal.Util.createBatcher({
|
||||
name: 'Whisper.deliveryReceiptBatcher',
|
||||
wait: 500,
|
||||
maxSize: 500,
|
||||
processBatch: async items => {
|
||||
const byConversationId = window._.groupBy(items, item =>
|
||||
window.ConversationController.ensureContactIds({
|
||||
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
|
||||
window.Whisper.deliveryReceiptBatcher = window.Signal.Util.createBatcher<DeliveryReceiptBatcherItemType>(
|
||||
{
|
||||
name: 'Whisper.deliveryReceiptBatcher',
|
||||
wait: 500,
|
||||
maxSize: 500,
|
||||
processBatch: async items => {
|
||||
const byConversationId = window._.groupBy(items, item =>
|
||||
window.ConversationController.ensureContactIds({
|
||||
e164: item.source,
|
||||
uuid: item.sourceUuid,
|
||||
})
|
||||
);
|
||||
const ids = Object.keys(byConversationId);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const c = window.ConversationController.get(conversationId)!;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const uuid = c.get('uuid')!;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const e164 = c.get('e164')!;
|
||||
for (let i = 0, max = ids.length; i < max; i += 1) {
|
||||
const conversationId = ids[i];
|
||||
const ourItems = byConversationId[conversationId];
|
||||
const timestamps = ourItems.map(item => item.timestamp);
|
||||
const messageIds = ourItems.map(item => item.messageId);
|
||||
|
||||
c.queueJob('sendDeliveryReceipt', async () => {
|
||||
try {
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = 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
|
||||
const c = window.ConversationController.get(conversationId);
|
||||
if (!c) {
|
||||
window.log.warn(
|
||||
`deliveryReceiptBatcher: Conversation ${conversationId} does not exist! ` +
|
||||
`Will not send delivery receipts for timestamps ${timestamps}`
|
||||
);
|
||||
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) {
|
||||
window.addEventListener('dblclick', (event: Event) => {
|
||||
|
@ -899,25 +900,47 @@ export async function startApp(): Promise<void> {
|
|||
window.Signal.Services.retryPlaceholders = retryPlaceholders;
|
||||
|
||||
setInterval(async () => {
|
||||
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,
|
||||
})
|
||||
const now = Date.now();
|
||||
const HOUR = 1000 * 60 * 60;
|
||||
const DAY = 24 * HOUR;
|
||||
const oneDayAgo = now - DAY;
|
||||
try {
|
||||
await window.Signal.Data.deleteSentProtosOlderThan(oneDayAgo);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'background/onready/setInterval: Error deleting sent protos: ',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
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);
|
||||
|
||||
try {
|
||||
|
@ -1640,7 +1663,18 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
function runStorageService() {
|
||||
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;
|
||||
|
@ -1868,7 +1902,18 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
|
||||
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,
|
||||
'gv1-migration': true,
|
||||
senderKey: window.Signal.RemoteConfig.isEnabled(
|
||||
'desktop.sendSenderKey'
|
||||
'desktop.sendSenderKey2'
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -2312,11 +2357,8 @@ export async function startApp(): Promise<void> {
|
|||
runStorageService();
|
||||
});
|
||||
|
||||
const ourId = window.ConversationController.getOurConversationId();
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = await window.ConversationController.prepareForSend(ourId, {
|
||||
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||
syncMessage: true,
|
||||
});
|
||||
|
||||
|
@ -2328,11 +2370,19 @@ export async function startApp(): Promise<void> {
|
|||
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(
|
||||
operations,
|
||||
sendOptions
|
||||
)
|
||||
),
|
||||
{ messageIds: [], sendType: 'otherSync' }
|
||||
).catch(error => {
|
||||
window.log.error(
|
||||
'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');
|
||||
}
|
||||
|
||||
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) {
|
||||
ev.confirm();
|
||||
|
||||
|
@ -4025,7 +3699,13 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
|
||||
function onReadReceipt(ev: ReadEvent) {
|
||||
const { envelopeTimestamp, timestamp, source, sourceUuid } = ev.read;
|
||||
const {
|
||||
envelopeTimestamp,
|
||||
timestamp,
|
||||
source,
|
||||
sourceUuid,
|
||||
sourceDevice,
|
||||
} = ev.read;
|
||||
const readAt = envelopeTimestamp;
|
||||
const reader = window.ConversationController.ensureContactIds({
|
||||
e164: source,
|
||||
|
@ -4036,6 +3716,7 @@ export async function startApp(): Promise<void> {
|
|||
'read receipt',
|
||||
source,
|
||||
sourceUuid,
|
||||
sourceDevice,
|
||||
envelopeTimestamp,
|
||||
reader,
|
||||
'for sent message',
|
||||
|
@ -4050,6 +3731,7 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
const receipt = ReadReceipts.getSingleton().add({
|
||||
reader,
|
||||
readerDevice: sourceDevice,
|
||||
timestamp,
|
||||
readAt,
|
||||
});
|
||||
|
@ -4198,6 +3880,7 @@ export async function startApp(): Promise<void> {
|
|||
const receipt = DeliveryReceipts.getSingleton().add({
|
||||
timestamp,
|
||||
deliveredTo,
|
||||
deliveredToDevice: sourceDevice,
|
||||
});
|
||||
|
||||
// Note: We don't wait for completion here
|
||||
|
|
59
ts/groups.ts
59
ts/groups.ts
|
@ -69,7 +69,7 @@ import {
|
|||
isGroupV2 as getIsGroupV2,
|
||||
isMe,
|
||||
} from './util/whatTypeOfConversation';
|
||||
import { handleMessageSend } from './util/handleMessageSend';
|
||||
import { handleMessageSend, SendTypesType } from './util/handleMessageSend';
|
||||
import { getSendOptions } from './util/getSendOptions';
|
||||
import * as Bytes from './Bytes';
|
||||
import { SignalService as Proto } from './protobuf';
|
||||
|
@ -1309,9 +1309,12 @@ export async function modifyGroupV2({
|
|||
profileKey,
|
||||
},
|
||||
conversation,
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
messageId: undefined,
|
||||
sendOptions,
|
||||
})
|
||||
sendType: 'groupChange',
|
||||
}),
|
||||
{ messageIds: [], sendType: 'groupChange' }
|
||||
);
|
||||
|
||||
// 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({
|
||||
conversation,
|
||||
logId: `sendToGroup/${logId}`,
|
||||
messageIds: [],
|
||||
send: async () =>
|
||||
window.Signal.Util.sendToGroup({
|
||||
groupSendOptions: {
|
||||
|
@ -1690,9 +1694,12 @@ export async function createGroupV2({
|
|||
profileKey,
|
||||
},
|
||||
conversation,
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
messageId: undefined,
|
||||
sendOptions,
|
||||
sendType: 'groupChange',
|
||||
}),
|
||||
sendType: 'groupChange',
|
||||
timestamp,
|
||||
});
|
||||
|
||||
|
@ -2212,6 +2219,7 @@ export async function initiateMigrationToGroupV2(
|
|||
await wrapWithSyncMessageSend({
|
||||
conversation,
|
||||
logId: `sendToGroup/${logId}`,
|
||||
messageIds: [],
|
||||
send: async () =>
|
||||
// Minimal message to notify group members about migration
|
||||
window.Signal.Util.sendToGroup({
|
||||
|
@ -2223,9 +2231,12 @@ export async function initiateMigrationToGroupV2(
|
|||
profileKey: ourProfileKey,
|
||||
},
|
||||
conversation,
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
messageId: undefined,
|
||||
sendOptions,
|
||||
sendType: 'groupChange',
|
||||
}),
|
||||
sendType: 'groupChange',
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
@ -2233,12 +2244,16 @@ export async function initiateMigrationToGroupV2(
|
|||
export async function wrapWithSyncMessageSend({
|
||||
conversation,
|
||||
logId,
|
||||
messageIds,
|
||||
send,
|
||||
sendType,
|
||||
timestamp,
|
||||
}: {
|
||||
conversation: ConversationModel;
|
||||
logId: string;
|
||||
send: (sender: MessageSender) => Promise<CallbackResultType | undefined>;
|
||||
messageIds: Array<string>;
|
||||
send: (sender: MessageSender) => Promise<CallbackResultType>;
|
||||
sendType: SendTypesType;
|
||||
timestamp: number;
|
||||
}): Promise<void> {
|
||||
const sender = window.textsecure.messaging;
|
||||
|
@ -2250,7 +2265,7 @@ export async function wrapWithSyncMessageSend({
|
|||
|
||||
let response: CallbackResultType | undefined;
|
||||
try {
|
||||
response = await send(sender);
|
||||
response = await handleMessageSend(send(sender), { messageIds, sendType });
|
||||
} catch (error) {
|
||||
if (conversation.processSendResponse(error)) {
|
||||
response = error;
|
||||
|
@ -2285,15 +2300,27 @@ export async function wrapWithSyncMessageSend({
|
|||
);
|
||||
}
|
||||
|
||||
await sender.sendSyncMessage({
|
||||
encodedDataMessage: dataMessage,
|
||||
timestamp,
|
||||
destination: ourConversation.get('e164'),
|
||||
destinationUuid: ourConversation.get('uuid'),
|
||||
expirationStartTimestamp: null,
|
||||
sentTo: [],
|
||||
unidentifiedDeliveries: [],
|
||||
});
|
||||
if (window.ConversationController.areWePrimaryDevice()) {
|
||||
window.log.warn(
|
||||
`wrapWithSyncMessageSend/${logId}: We are primary device; not sync message`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
|
|
|
@ -10,10 +10,15 @@ import { ConversationModel } from '../models/conversations';
|
|||
import { MessageModel } from '../models/messages';
|
||||
import { MessageModelCollectionType } from '../model-types.d';
|
||||
import { isIncoming } from '../state/selectors/message';
|
||||
import { isDirectConversation } from '../util/whatTypeOfConversation';
|
||||
import dataInterface from '../sql/Client';
|
||||
|
||||
const { deleteSentProtoRecipient } = dataInterface;
|
||||
|
||||
type DeliveryReceiptAttributesType = {
|
||||
timestamp: number;
|
||||
deliveredTo: string;
|
||||
deliveredToDevice: number;
|
||||
};
|
||||
|
||||
class DeliveryReceiptModel extends Model<DeliveryReceiptAttributesType> {}
|
||||
|
@ -67,7 +72,7 @@ export class DeliveryReceipts extends Collection<DeliveryReceiptModel> {
|
|||
message: MessageModel
|
||||
): Array<DeliveryReceiptModel> {
|
||||
let recipients: Array<string>;
|
||||
if (conversation.isPrivate()) {
|
||||
if (isDirectConversation(conversation.attributes)) {
|
||||
recipients = [conversation.id];
|
||||
} else {
|
||||
recipients = conversation.getMemberIds();
|
||||
|
@ -82,32 +87,29 @@ export class DeliveryReceipts extends Collection<DeliveryReceiptModel> {
|
|||
}
|
||||
|
||||
async onReceipt(receipt: DeliveryReceiptModel): Promise<void> {
|
||||
try {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||
receipt.get('timestamp'),
|
||||
{
|
||||
MessageCollection: window.Whisper.MessageCollection,
|
||||
}
|
||||
);
|
||||
const timestamp = receipt.get('timestamp');
|
||||
const deliveredTo = receipt.get('deliveredTo');
|
||||
|
||||
const message = await getTargetMessage(
|
||||
receipt.get('deliveredTo'),
|
||||
messages
|
||||
);
|
||||
try {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(timestamp, {
|
||||
MessageCollection: window.Whisper.MessageCollection,
|
||||
});
|
||||
|
||||
const message = await getTargetMessage(deliveredTo, messages);
|
||||
if (!message) {
|
||||
window.log.info(
|
||||
'No message for delivery receipt',
|
||||
receipt.get('deliveredTo'),
|
||||
receipt.get('timestamp')
|
||||
deliveredTo,
|
||||
timestamp
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const deliveries = message.get('delivered') || 0;
|
||||
const deliveredTo = message.get('delivered_to') || [];
|
||||
const originalDeliveredTo = message.get('delivered_to') || [];
|
||||
const expirationStartTimestamp = message.get('expirationStartTimestamp');
|
||||
message.set({
|
||||
delivered_to: union(deliveredTo, [receipt.get('deliveredTo')]),
|
||||
delivered_to: union(originalDeliveredTo, [deliveredTo]),
|
||||
delivered: deliveries + 1,
|
||||
expirationStartTimestamp: expirationStartTimestamp || Date.now(),
|
||||
sent: true,
|
||||
|
@ -126,6 +128,33 @@ export class DeliveryReceipts extends Collection<DeliveryReceiptModel> {
|
|||
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);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
|
|
|
@ -9,9 +9,14 @@ import { ConversationModel } from '../models/conversations';
|
|||
import { MessageModel } from '../models/messages';
|
||||
import { MessageModelCollectionType } from '../model-types.d';
|
||||
import { isOutgoing } from '../state/selectors/message';
|
||||
import { isDirectConversation } from '../util/whatTypeOfConversation';
|
||||
import dataInterface from '../sql/Client';
|
||||
|
||||
const { deleteSentProtoRecipient } = dataInterface;
|
||||
|
||||
type ReadReceiptAttributesType = {
|
||||
reader: string;
|
||||
readerDevice: number;
|
||||
timestamp: number;
|
||||
readAt: number;
|
||||
};
|
||||
|
@ -68,7 +73,7 @@ export class ReadReceipts extends Collection<ReadReceiptModel> {
|
|||
return [];
|
||||
}
|
||||
let ids: Array<string>;
|
||||
if (conversation.isPrivate()) {
|
||||
if (isDirectConversation(conversation.attributes)) {
|
||||
ids = [conversation.id];
|
||||
} else {
|
||||
ids = conversation.getMemberIds();
|
||||
|
@ -86,29 +91,25 @@ export class ReadReceipts extends Collection<ReadReceiptModel> {
|
|||
}
|
||||
|
||||
async onReceipt(receipt: ReadReceiptModel): Promise<void> {
|
||||
try {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||
receipt.get('timestamp'),
|
||||
{
|
||||
MessageCollection: window.Whisper.MessageCollection,
|
||||
}
|
||||
);
|
||||
const timestamp = receipt.get('timestamp');
|
||||
const reader = receipt.get('reader');
|
||||
|
||||
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) {
|
||||
window.log.info(
|
||||
'No message for read receipt',
|
||||
receipt.get('reader'),
|
||||
receipt.get('timestamp')
|
||||
);
|
||||
window.log.info('No message for read receipt', reader, timestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
const readBy = message.get('read_by') || [];
|
||||
const expirationStartTimestamp = message.get('expirationStartTimestamp');
|
||||
|
||||
readBy.push(receipt.get('reader'));
|
||||
readBy.push(reader);
|
||||
message.set({
|
||||
read_by: readBy,
|
||||
expirationStartTimestamp: expirationStartTimestamp || Date.now(),
|
||||
|
@ -128,6 +129,22 @@ export class ReadReceipts extends Collection<ReadReceiptModel> {
|
|||
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);
|
||||
} catch (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;
|
||||
fromSync?: boolean;
|
||||
};
|
||||
|
||||
export declare class ReactionModelType extends Backbone.Model<ReactionAttributesType> {}
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
MessageAttributesType,
|
||||
MessageModelCollectionType,
|
||||
QuotedMessageType,
|
||||
ReactionModelType,
|
||||
VerificationOptions,
|
||||
WhatIsThis,
|
||||
} from '../model-types.d';
|
||||
|
@ -64,7 +63,6 @@ import {
|
|||
isGroupV2,
|
||||
isMe,
|
||||
} from '../util/whatTypeOfConversation';
|
||||
import { deprecated } from '../util/deprecated';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import {
|
||||
hasErrors,
|
||||
|
@ -73,7 +71,7 @@ import {
|
|||
getMessagePropStatus,
|
||||
} from '../state/selectors/message';
|
||||
import { Deletes } from '../messageModifiers/Deletes';
|
||||
import { Reactions } from '../messageModifiers/Reactions';
|
||||
import { Reactions, ReactionModel } from '../messageModifiers/Reactions';
|
||||
|
||||
// TODO: remove once we move away from ArrayBuffers
|
||||
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 {
|
||||
if (!isGroupV2(this.attributes)) {
|
||||
return false;
|
||||
|
@ -1200,7 +1193,8 @@ export class ConversationModel extends window.Backbone
|
|||
...sendOptions,
|
||||
online: true,
|
||||
},
|
||||
})
|
||||
}),
|
||||
{ messageIds: [], sendType: 'typing' }
|
||||
);
|
||||
} else {
|
||||
handleMessageSend(
|
||||
|
@ -1208,11 +1202,14 @@ export class ConversationModel extends window.Backbone
|
|||
contentHint: ContentHint.IMPLICIT,
|
||||
contentMessage,
|
||||
conversation: this,
|
||||
messageId: undefined,
|
||||
online: true,
|
||||
recipients: groupMembers,
|
||||
sendOptions,
|
||||
sendType: 'typing',
|
||||
timestamp,
|
||||
})
|
||||
}),
|
||||
{ messageIds: [], sendType: 'typing' }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -1577,6 +1574,7 @@ export class ConversationModel extends window.Backbone
|
|||
m => !hasErrors(m.attributes) && isIncoming(m.attributes)
|
||||
);
|
||||
const receiptSpecs = readMessages.map(m => ({
|
||||
messageId: m.id,
|
||||
senderE164: m.get('source'),
|
||||
senderUuid: m.get('sourceUuid'),
|
||||
senderId: window.ConversationController.ensureContactIds({
|
||||
|
@ -1988,22 +1986,22 @@ export class ConversationModel extends window.Backbone
|
|||
// server updates were successful.
|
||||
await this.applyMessageRequestResponse(response);
|
||||
|
||||
const { ourNumber, ourUuid } = this;
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = await window.ConversationController.prepareForSend(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
ourNumber || ourUuid!,
|
||||
{
|
||||
syncMessage: true,
|
||||
}
|
||||
);
|
||||
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||
syncMessage: true,
|
||||
});
|
||||
|
||||
const groupId = this.getGroupIdBuffer();
|
||||
|
||||
if (window.ConversationController.areWePrimaryDevice()) {
|
||||
window.log.warn(
|
||||
'syncMessageRequestResponse: We are primary device; not sending message request sync'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await wrap(
|
||||
await handleMessageSend(
|
||||
window.textsecure.messaging.syncMessageRequestResponse(
|
||||
{
|
||||
threadE164: this.get('e164'),
|
||||
|
@ -2012,7 +2010,8 @@ export class ConversationModel extends window.Backbone
|
|||
type: response,
|
||||
},
|
||||
sendOptions
|
||||
)
|
||||
),
|
||||
{ messageIds: [], sendType: 'otherSync' }
|
||||
);
|
||||
} catch (result) {
|
||||
this.processSendResponse(result);
|
||||
|
@ -2167,10 +2166,8 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
if (!options.viaSyncMessage) {
|
||||
await this.sendVerifySyncMessage(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.get('e164')!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.get('uuid')!,
|
||||
this.get('e164'),
|
||||
this.get('uuid'),
|
||||
verified
|
||||
);
|
||||
}
|
||||
|
@ -2179,33 +2176,52 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
|
||||
async sendVerifySyncMessage(
|
||||
e164: string,
|
||||
uuid: string,
|
||||
e164: string | undefined,
|
||||
uuid: string | undefined,
|
||||
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
|
||||
// a sync message to our own devices, we need to send the accessKeys down for both
|
||||
// contacts. So we merge their sendOptions.
|
||||
const { sendOptions } = await window.ConversationController.prepareForSend(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.ourNumber || this.ourUuid!,
|
||||
{ syncMessage: true }
|
||||
);
|
||||
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||
syncMessage: true,
|
||||
});
|
||||
const contactSendOptions = await getSendOptions(this.attributes);
|
||||
const options = { ...sendOptions, ...contactSendOptions };
|
||||
|
||||
const promise = window.textsecure.storage.protocol.loadIdentityKey(e164);
|
||||
return promise.then(key =>
|
||||
handleMessageSend(
|
||||
window.textsecure.messaging.syncVerification(
|
||||
e164,
|
||||
uuid,
|
||||
state,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
key!,
|
||||
options
|
||||
)
|
||||
)
|
||||
const key = await window.textsecure.storage.protocol.loadIdentityKey(
|
||||
identifier
|
||||
);
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
`sendVerifySyncMessage: No identity key found for identifier ${identifier}`
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return this.contactCollection!.every(contact => {
|
||||
return this.contactCollection?.every(contact => {
|
||||
if (isMe(contact.attributes)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -2238,16 +2253,12 @@ export class ConversationModel extends window.Backbone
|
|||
verified !== this.verifiedEnum!.DEFAULT
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
if (!this.contactCollection!.length) {
|
||||
|
||||
if (!this.contactCollection?.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Array.any does not exist. This is probably broken.
|
||||
// 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 => {
|
||||
return this.contactCollection?.some(contact => {
|
||||
if (isMe(contact.attributes)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -2262,8 +2273,7 @@ export class ConversationModel extends window.Backbone
|
|||
: 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)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -3158,7 +3168,11 @@ export class ConversationModel extends window.Backbone
|
|||
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();
|
||||
|
||||
if (timestamp - targetTimestamp > THREE_HOURS) {
|
||||
|
@ -3224,7 +3238,7 @@ export class ConversationModel extends window.Backbone
|
|||
deletedForEveryoneTimestamp: targetTimestamp,
|
||||
timestamp,
|
||||
expireTimer: undefined,
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
groupId: undefined,
|
||||
profileKey,
|
||||
options: sendOptions,
|
||||
|
@ -3240,8 +3254,10 @@ export class ConversationModel extends window.Backbone
|
|||
profileKey,
|
||||
},
|
||||
conversation: this,
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
messageId,
|
||||
sendOptions,
|
||||
sendType: 'deleteForEveryone',
|
||||
});
|
||||
})();
|
||||
|
||||
|
@ -3249,11 +3265,16 @@ export class ConversationModel extends window.Backbone
|
|||
// anything to the database.
|
||||
message.doNotSave = true;
|
||||
|
||||
const result = await message.send(handleMessageSend(promise));
|
||||
const result = await message.send(
|
||||
handleMessageSend(promise, {
|
||||
messageIds: [messageId],
|
||||
sendType: 'deleteForEveryone',
|
||||
})
|
||||
);
|
||||
|
||||
if (!message.hasSuccessfulDelivery()) {
|
||||
// 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');
|
||||
}
|
||||
Deletes.getSingleton().onDelete(deleteModel);
|
||||
|
@ -3274,10 +3295,12 @@ export class ConversationModel extends window.Backbone
|
|||
async sendReactionMessage(
|
||||
reaction: { emoji: string; remove: boolean },
|
||||
target: {
|
||||
messageId: string;
|
||||
targetAuthorUuid: string;
|
||||
targetTimestamp: number;
|
||||
}
|
||||
): Promise<WhatIsThis> {
|
||||
const { messageId } = target;
|
||||
const timestamp = Date.now();
|
||||
const outgoingReaction = { ...reaction, ...target };
|
||||
|
||||
|
@ -3373,7 +3396,7 @@ export class ConversationModel extends window.Backbone
|
|||
deletedForEveryoneTimestamp: undefined,
|
||||
timestamp,
|
||||
expireTimer,
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
groupId: undefined,
|
||||
profileKey,
|
||||
options,
|
||||
|
@ -3392,12 +3415,19 @@ export class ConversationModel extends window.Backbone
|
|||
profileKey,
|
||||
},
|
||||
conversation: this,
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
messageId,
|
||||
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()) {
|
||||
// This is handled by `conversation_view` which displays a toast on
|
||||
|
@ -3407,7 +3437,7 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
return result;
|
||||
}).catch(() => {
|
||||
let reverseReaction: ReactionModelType;
|
||||
let reverseReaction: ReactionModel;
|
||||
if (oldReaction) {
|
||||
// Either restore old reaction
|
||||
reverseReaction = Reactions.getSingleton().add({
|
||||
|
@ -3444,11 +3474,15 @@ export class ConversationModel extends window.Backbone
|
|||
);
|
||||
return;
|
||||
}
|
||||
await window.textsecure.messaging.sendProfileKeyUpdate(
|
||||
profileKey,
|
||||
recipients,
|
||||
await getSendOptions(this.attributes),
|
||||
this.get('groupId')
|
||||
|
||||
await handleMessageSend(
|
||||
window.textsecure.messaging.sendProfileKeyUpdate(
|
||||
profileKey,
|
||||
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);
|
||||
}
|
||||
const message = window.MessageController.register(model.id, model);
|
||||
const messageId = message.id;
|
||||
await window.Signal.Data.saveMessage(message.attributes, {
|
||||
forceSave: true,
|
||||
Message: window.Whisper.Message,
|
||||
|
@ -3635,7 +3670,9 @@ export class ConversationModel extends window.Backbone
|
|||
},
|
||||
conversation: this,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
messageId,
|
||||
sendOptions: options,
|
||||
sendType: 'message',
|
||||
});
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
@ -4220,7 +4267,8 @@ export class ConversationModel extends window.Backbone
|
|||
groupId,
|
||||
groupIdentifiers,
|
||||
options
|
||||
)
|
||||
),
|
||||
{ messageIds: [], sendType: 'legacyGroupChange' }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -167,7 +167,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
|
||||
isSelected?: boolean;
|
||||
|
||||
syncPromise?: Promise<unknown>;
|
||||
syncPromise?: Promise<CallbackResultType | void>;
|
||||
|
||||
initialize(attributes: unknown): void {
|
||||
if (_.isObject(attributes)) {
|
||||
|
@ -774,8 +774,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
const { messageDeleted } = window.reduxActions.conversations;
|
||||
messageDeleted(this.id, this.get('conversationId'));
|
||||
window.reduxActions?.conversations?.messageDeleted(
|
||||
this.id,
|
||||
this.get('conversationId')
|
||||
);
|
||||
|
||||
this.getConversation()?.debouncedUpdateLastMessage?.();
|
||||
|
||||
|
@ -868,26 +870,26 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
|
||||
const timestamp = this.get('sent_at');
|
||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const ourUuid = window.textsecure.storage.user.getUuid()!;
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = await window.ConversationController.prepareForSend(
|
||||
ourNumber || ourUuid,
|
||||
{
|
||||
syncMessage: true,
|
||||
}
|
||||
);
|
||||
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||
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(
|
||||
sender,
|
||||
senderUuid,
|
||||
timestamp,
|
||||
sendOptions
|
||||
)
|
||||
),
|
||||
{ messageIds: [this.id], sendType: 'viewOnceSync' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -987,6 +989,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
Message: window.Whisper.Message,
|
||||
});
|
||||
}
|
||||
|
||||
await window.Signal.Data.deleteSentProtoByMessageId(this.id);
|
||||
}
|
||||
|
||||
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
|
||||
// entire member list.
|
||||
isPartialSend: true,
|
||||
messageId: this.id,
|
||||
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
|
||||
|
@ -1429,10 +1440,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
const parentConversation = this.getConversation();
|
||||
const groupId = parentConversation?.get('groupId');
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = await window.ConversationController.prepareForSend(identifier);
|
||||
|
||||
const recipientConversation = window.ConversationController.get(identifier);
|
||||
const sendOptions = recipientConversation
|
||||
? await getSendOptions(recipientConversation.attributes)
|
||||
: undefined;
|
||||
const group =
|
||||
groupId && isGroupV1(parentConversation?.attributes)
|
||||
? {
|
||||
|
@ -1479,7 +1491,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
options: sendOptions,
|
||||
});
|
||||
|
||||
return this.send(wrap(promise));
|
||||
return this.send(
|
||||
handleMessageSend(promise, {
|
||||
messageIds: [this.id],
|
||||
sendType: 'messageRetry',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
removeOutgoingErrors(incomingIdentifier: string): CustomError {
|
||||
|
@ -1689,18 +1706,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
// possible.
|
||||
await this.send(
|
||||
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(
|
||||
options.uuid,
|
||||
options.e164,
|
||||
options.now,
|
||||
sendOptions
|
||||
)
|
||||
),
|
||||
{ messageIds: [], sendType: 'resetSession' }
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -1725,10 +1737,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
sent: true,
|
||||
expirationStartTimestamp: Date.now(),
|
||||
});
|
||||
const result: typeof window.WhatIsThis = await this.sendSyncMessage();
|
||||
const result = await this.sendSyncMessage();
|
||||
this.set({
|
||||
// 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
|
||||
// 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> {
|
||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = await window.ConversationController.prepareForSend(
|
||||
ourUuid || ourNumber,
|
||||
{
|
||||
syncMessage: true,
|
||||
}
|
||||
);
|
||||
async sendSyncMessage(): Promise<CallbackResultType | void> {
|
||||
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||
syncMessage: true,
|
||||
});
|
||||
|
||||
if (window.ConversationController.areWePrimaryDevice()) {
|
||||
window.log.warn(
|
||||
'sendSyncMessage: We are primary device; not sending sync message'
|
||||
);
|
||||
this.set({ dataMessage: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncPromise = this.syncPromise || Promise.resolve();
|
||||
const next = async () => {
|
||||
const dataMessage = this.get('dataMessage');
|
||||
if (!dataMessage) {
|
||||
return Promise.resolve();
|
||||
return;
|
||||
}
|
||||
const isUpdate = Boolean(this.get('synced'));
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const conv = this.getConversation()!;
|
||||
|
||||
return wrap(
|
||||
return handleMessageSend(
|
||||
window.textsecure.messaging.sendSyncMessage({
|
||||
encodedDataMessage: dataMessage,
|
||||
timestamp: this.get('sent_at'),
|
||||
|
@ -1786,8 +1802,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
unidentifiedDeliveries: this.get('unidentifiedDeliveries') || [],
|
||||
isUpdate,
|
||||
options: sendOptions,
|
||||
})
|
||||
).then(async (result: unknown) => {
|
||||
}),
|
||||
{ messageIds: [this.id], sendType: 'sentSync' }
|
||||
).then(async result => {
|
||||
this.set({
|
||||
synced: true,
|
||||
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
|
||||
|
||||
if (initialMessage.groupV2) {
|
||||
|
@ -2640,6 +2635,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return;
|
||||
}
|
||||
|
||||
const messageId = window.getGuid();
|
||||
|
||||
// Send delivery receipts, but only for incoming sealed sender messages
|
||||
// and not for messages from unaccepted conversations
|
||||
if (
|
||||
|
@ -2653,6 +2650,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
// The queue can be paused easily.
|
||||
window.Whisper.deliveryReceiptQueue.add(() => {
|
||||
window.Whisper.deliveryReceiptBatcher.add({
|
||||
messageId,
|
||||
source,
|
||||
sourceUuid,
|
||||
timestamp: this.get('sent_at'),
|
||||
|
@ -2689,7 +2687,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
|
||||
message.set({
|
||||
id: window.getGuid(),
|
||||
id: messageId,
|
||||
attachments: dataMessage.attachments,
|
||||
body: dataMessage.body,
|
||||
bodyRanges: dataMessage.bodyRanges,
|
||||
|
@ -3270,6 +3268,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
conversationId: this.get('conversationId'),
|
||||
emoji: reaction.get('emoji'),
|
||||
fromId: reaction.get('fromId'),
|
||||
messageId: this.id,
|
||||
messageReceivedAt: this.get('received_at'),
|
||||
targetAuthorUuid: reaction.get('targetAuthorUuid'),
|
||||
targetTimestamp: reaction.get('targetTimestamp'),
|
||||
|
|
|
@ -57,6 +57,7 @@ import {
|
|||
import { assert } from '../util/assert';
|
||||
import { dropNull, shallowDropNull } from '../util/dropNull';
|
||||
import { getOwn } from '../util/getOwn';
|
||||
import { handleMessageSend } from '../util/handleMessageSend';
|
||||
import {
|
||||
fetchMembershipProof,
|
||||
getMembershipList,
|
||||
|
@ -937,13 +938,17 @@ export class CallingClass {
|
|||
wrapWithSyncMessageSend({
|
||||
conversation,
|
||||
logId: `sendToGroup/groupCallUpdate/${conversationId}-${eraId}`,
|
||||
messageIds: [],
|
||||
send: () =>
|
||||
window.Signal.Util.sendToGroup({
|
||||
groupSendOptions: { groupCallUpdate: { eraId }, groupV2, timestamp },
|
||||
conversation,
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
messageId: undefined,
|
||||
sendOptions,
|
||||
sendType: 'callingMessage',
|
||||
}),
|
||||
sendType: 'callingMessage',
|
||||
timestamp,
|
||||
}).catch(err => {
|
||||
window.log.error(
|
||||
|
@ -1559,12 +1564,19 @@ export class CallingClass {
|
|||
}
|
||||
|
||||
try {
|
||||
await window.textsecure.messaging.sendCallingMessage(
|
||||
remoteUserId,
|
||||
callingMessageToProto(message),
|
||||
sendOptions
|
||||
const result = await handleMessageSend(
|
||||
window.textsecure.messaging.sendCallingMessage(
|
||||
remoteUserId,
|
||||
callingMessageToProto(message),
|
||||
sendOptions
|
||||
),
|
||||
{ messageIds: [], sendType: 'callingMessage' }
|
||||
);
|
||||
|
||||
if (result && result.errors && result.errors.length) {
|
||||
throw result.errors[0];
|
||||
}
|
||||
|
||||
window.log.info('handleOutgoingSignaling() completed successfully');
|
||||
return true;
|
||||
} catch (err) {
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
import { ConversationModel } from '../models/conversations';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { BackOff } from '../util/BackOff';
|
||||
import { handleMessageSend } from '../util/handleMessageSend';
|
||||
import { storageJobQueue } from '../util/JobQueue';
|
||||
import { sleep } from '../util/sleep';
|
||||
import { isMoreRecentThan } from '../util/timestamp';
|
||||
|
@ -531,7 +532,18 @@ async function uploadManifest(
|
|||
window.storage.put('manifestVersion', version);
|
||||
conflictBackOff.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() {
|
||||
|
@ -552,7 +564,18 @@ async function stopStorageServiceSync() {
|
|||
if (!window.textsecure.messaging) {
|
||||
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'
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { handleMessageSend } from '../util/handleMessageSend';
|
||||
import { getSendOptions } from '../util/getSendOptions';
|
||||
|
||||
export async function sendStickerPackSync(
|
||||
packId: string,
|
||||
packKey: string,
|
||||
installed: boolean
|
||||
): Promise<void> {
|
||||
const { ConversationController, textsecure, log } = window;
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const { wrap, sendOptions } = await ConversationController.prepareForSend(
|
||||
ourNumber,
|
||||
{
|
||||
syncMessage: true,
|
||||
}
|
||||
);
|
||||
const ourConversation = ConversationController.getOurConversationOrThrow();
|
||||
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||
syncMessage: true,
|
||||
});
|
||||
|
||||
if (!textsecure.messaging) {
|
||||
log.error(
|
||||
|
@ -23,7 +23,14 @@ export async function sendStickerPackSync(
|
|||
return;
|
||||
}
|
||||
|
||||
wrap(
|
||||
if (window.ConversationController.areWePrimaryDevice()) {
|
||||
window.log.warn(
|
||||
'shims/sendStickerPackSync: We are primary device; not sending sync'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
handleMessageSend(
|
||||
textsecure.messaging.sendStickerPackSync(
|
||||
[
|
||||
{
|
||||
|
@ -33,7 +40,8 @@ export async function sendStickerPackSync(
|
|||
},
|
||||
],
|
||||
sendOptions
|
||||
)
|
||||
),
|
||||
{ messageIds: [], sendType: 'otherSync' }
|
||||
).catch(error => {
|
||||
log.error(
|
||||
'shim: Error calling sendStickerPackSync:',
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
cloneDeep,
|
||||
compact,
|
||||
fromPairs,
|
||||
toPairs,
|
||||
get,
|
||||
groupBy,
|
||||
isFunction,
|
||||
|
@ -22,6 +21,8 @@ import {
|
|||
map,
|
||||
omit,
|
||||
set,
|
||||
toPairs,
|
||||
uniq,
|
||||
} from 'lodash';
|
||||
|
||||
import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto';
|
||||
|
@ -41,8 +42,8 @@ import { StoredJob } from '../jobs/types';
|
|||
import {
|
||||
AttachmentDownloadJobType,
|
||||
ClientInterface,
|
||||
ClientSearchResultMessageType,
|
||||
ClientJobType,
|
||||
ClientSearchResultMessageType,
|
||||
ConversationType,
|
||||
IdentityKeyType,
|
||||
ItemKeyType,
|
||||
|
@ -52,6 +53,12 @@ import {
|
|||
PreKeyType,
|
||||
SearchResultMessageType,
|
||||
SenderKeyType,
|
||||
SentMessageDBType,
|
||||
SentMessagesType,
|
||||
SentProtoType,
|
||||
SentProtoWithMessageIdsType,
|
||||
SentRecipientsDBType,
|
||||
SentRecipientsType,
|
||||
ServerInterface,
|
||||
SessionType,
|
||||
SignedPreKeyType,
|
||||
|
@ -143,6 +150,17 @@ const dataInterface: ClientInterface = {
|
|||
getAllSenderKeys,
|
||||
removeSenderKeyById,
|
||||
|
||||
insertSentProto,
|
||||
deleteSentProtosOlderThan,
|
||||
deleteSentProtoByMessageId,
|
||||
insertProtoRecipients,
|
||||
deleteSentProtoRecipient,
|
||||
getSentProtoByRecipient,
|
||||
removeAllSentProtos,
|
||||
getAllSentProtos,
|
||||
_getAllSentProtoRecipients,
|
||||
_getAllSentProtoMessageIds,
|
||||
|
||||
createOrUpdateSession,
|
||||
createOrUpdateSessions,
|
||||
commitSessionsAndUnprocessed,
|
||||
|
@ -771,6 +789,66 @@ async function removeSenderKeyById(id: string): Promise<void> {
|
|||
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
|
||||
|
||||
async function createOrUpdateSession(data: SessionType) {
|
||||
|
|
|
@ -17,6 +17,7 @@ import type { ReactionType } from '../types/Reactions';
|
|||
import type { ConversationColorType, CustomColorType } from '../types/Colors';
|
||||
import { StorageAccessType } from '../types/Storage.d';
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import { BodyRangesType } from '../types/Util';
|
||||
|
||||
export type AttachmentDownloadJobTypeType =
|
||||
| 'long-message'
|
||||
|
@ -83,9 +84,32 @@ export type SearchResultMessageType = {
|
|||
};
|
||||
export type ClientSearchResultMessageType = MessageType & {
|
||||
json: string;
|
||||
bodyRanges: [];
|
||||
bodyRanges: BodyRangesType;
|
||||
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 = {
|
||||
// Primary key
|
||||
id: string;
|
||||
|
@ -215,6 +239,36 @@ export type DataInterface = {
|
|||
getAllSenderKeys: () => Promise<Array<SenderKeyType>>;
|
||||
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>;
|
||||
createOrUpdateSessions: (array: Array<SessionType>) => Promise<void>;
|
||||
commitSessionsAndUnprocessed(options: {
|
||||
|
@ -255,6 +309,36 @@ export type DataInterface = {
|
|||
) => Promise<void>;
|
||||
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>;
|
||||
getAllUnprocessed: () => Promise<Array<UnprocessedType>>;
|
||||
updateUnprocessedAttempts: (id: string, attempts: number) => Promise<void>;
|
||||
|
@ -391,33 +475,6 @@ export type ServerInterface = DataInterface & {
|
|||
ourConversationId: string;
|
||||
}) => Promise<MessageType | undefined>;
|
||||
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>;
|
||||
removeMessage: (id: string) => Promise<void>;
|
||||
removeMessages: (ids: Array<string>) => Promise<void>;
|
||||
|
@ -530,33 +587,6 @@ export type ClientInterface = DataInterface & {
|
|||
getTapToViewMessagesNeedingErase: (options: {
|
||||
MessageCollection: typeof 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: (
|
||||
id: string,
|
||||
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 { isNormalNumber } from '../util/isNormalNumber';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
||||
import { ConversationColorType, CustomColorType } from '../types/Colors';
|
||||
|
||||
import {
|
||||
AllItemsType,
|
||||
AttachmentDownloadJobType,
|
||||
ConversationMetricsType,
|
||||
ConversationType,
|
||||
EmojiType,
|
||||
IdentityKeyType,
|
||||
AllItemsType,
|
||||
ItemKeyType,
|
||||
ItemType,
|
||||
MessageMetricsType,
|
||||
MessageType,
|
||||
MessageTypeUnhydrated,
|
||||
MessageMetricsType,
|
||||
PreKeyType,
|
||||
SearchResultMessageType,
|
||||
SenderKeyType,
|
||||
SentMessageDBType,
|
||||
SentMessagesType,
|
||||
SentProtoType,
|
||||
SentProtoWithMessageIdsType,
|
||||
SentRecipientsDBType,
|
||||
SentRecipientsType,
|
||||
ServerInterface,
|
||||
SessionType,
|
||||
SignedPreKeyType,
|
||||
|
@ -63,14 +70,6 @@ import {
|
|||
UnprocessedUpdateType,
|
||||
} 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 ConversationRow = Readonly<{
|
||||
json: string;
|
||||
|
@ -137,6 +136,17 @@ const dataInterface: ServerInterface = {
|
|||
getAllSenderKeys,
|
||||
removeSenderKeyById,
|
||||
|
||||
insertSentProto,
|
||||
deleteSentProtosOlderThan,
|
||||
deleteSentProtoByMessageId,
|
||||
insertProtoRecipients,
|
||||
deleteSentProtoRecipient,
|
||||
getSentProtoByRecipient,
|
||||
removeAllSentProtos,
|
||||
getAllSentProtos,
|
||||
_getAllSentProtoRecipients,
|
||||
_getAllSentProtoMessageIds,
|
||||
|
||||
createOrUpdateSession,
|
||||
createOrUpdateSessions,
|
||||
commitSessionsAndUnprocessed,
|
||||
|
@ -253,16 +263,16 @@ type DatabaseQueryCache = Map<string, Statement<Array<any>>>;
|
|||
|
||||
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);
|
||||
if (!dbCache) {
|
||||
dbCache = new Map();
|
||||
statementCache.set(db, dbCache);
|
||||
}
|
||||
|
||||
let result = dbCache.get(query);
|
||||
let result = dbCache.get(query) as Statement<T>;
|
||||
if (!result) {
|
||||
result = db.prepare(query);
|
||||
result = db.prepare<T>(query);
|
||||
dbCache.set(query, result);
|
||||
}
|
||||
|
||||
|
@ -1947,6 +1957,84 @@ function updateToSchemaVersion36(currentVersion: number, db: Database) {
|
|||
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 = [
|
||||
updateToSchemaVersion1,
|
||||
updateToSchemaVersion2,
|
||||
|
@ -1984,6 +2072,7 @@ const SCHEMA_VERSIONS = [
|
|||
updateToSchemaVersion34,
|
||||
updateToSchemaVersion35,
|
||||
updateToSchemaVersion36,
|
||||
updateToSchemaVersion37,
|
||||
];
|
||||
|
||||
function updateSchema(db: Database): void {
|
||||
|
@ -2350,11 +2439,11 @@ async function getSenderKeyById(
|
|||
}
|
||||
async function removeAllSenderKeys(): Promise<void> {
|
||||
const db = getInstance();
|
||||
prepare(db, 'DELETE FROM senderKeys').run({});
|
||||
prepare<EmptyQuery>(db, 'DELETE FROM senderKeys').run();
|
||||
}
|
||||
async function getAllSenderKeys(): Promise<Array<SenderKeyType>> {
|
||||
const db = getInstance();
|
||||
const rows = prepare(db, 'SELECT * FROM senderKeys').all({});
|
||||
const rows = prepare<EmptyQuery>(db, 'SELECT * FROM senderKeys').all();
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
@ -2363,6 +2452,317 @@ async function removeSenderKeyById(id: string): Promise<void> {
|
|||
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';
|
||||
function createOrUpdateSessionSync(data: SessionType): void {
|
||||
const db = getInstance();
|
||||
|
@ -2717,8 +3117,7 @@ function updateConversationSync(data: ConversationType): void {
|
|||
? members.join(' ')
|
||||
: null;
|
||||
|
||||
prepare(
|
||||
db,
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE conversations SET
|
||||
json = $json,
|
||||
|
@ -3470,13 +3869,18 @@ async function getUnreadByConversationAndMarkRead(
|
|||
async function getUnreadReactionsAndMarkRead(
|
||||
conversationId: string,
|
||||
newestUnreadId: number
|
||||
): Promise<Array<Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp'>>> {
|
||||
): Promise<
|
||||
Array<
|
||||
Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp' | 'messageId'>
|
||||
>
|
||||
> {
|
||||
const db = getInstance();
|
||||
|
||||
return db.transaction(() => {
|
||||
const unreadMessages = db
|
||||
.prepare<Query>(
|
||||
`
|
||||
SELECT targetAuthorUuid, targetTimestamp
|
||||
SELECT targetAuthorUuid, targetTimestamp, messageId
|
||||
FROM reactions WHERE
|
||||
unread = 1 AND
|
||||
conversationId = $conversationId AND
|
||||
|
@ -3548,6 +3952,7 @@ async function addReaction({
|
|||
conversationId,
|
||||
emoji,
|
||||
fromId,
|
||||
messageId,
|
||||
messageReceivedAt,
|
||||
targetAuthorUuid,
|
||||
targetTimestamp,
|
||||
|
@ -3559,6 +3964,7 @@ async function addReaction({
|
|||
conversationId,
|
||||
emoji,
|
||||
fromId,
|
||||
messageId,
|
||||
messageReceivedAt,
|
||||
targetAuthorUuid,
|
||||
targetTimestamp,
|
||||
|
@ -3567,6 +3973,7 @@ async function addReaction({
|
|||
$conversationId,
|
||||
$emoji,
|
||||
$fromId,
|
||||
$messageId,
|
||||
$messageReceivedAt,
|
||||
$targetAuthorUuid,
|
||||
$targetTimestamp,
|
||||
|
@ -3577,6 +3984,7 @@ async function addReaction({
|
|||
conversationId,
|
||||
emoji,
|
||||
fromId,
|
||||
messageId,
|
||||
messageReceivedAt,
|
||||
targetAuthorUuid,
|
||||
targetTimestamp,
|
||||
|
|
|
@ -5,6 +5,7 @@ import { assert } from 'chai';
|
|||
import * as sinon from 'sinon';
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { CallbackResultType } from '../../textsecure/SendMessage';
|
||||
import { SignalService as Proto } from '../../protobuf';
|
||||
|
||||
describe('Message', () => {
|
||||
|
@ -71,7 +72,16 @@ describe('Message', () => {
|
|||
it('updates the `sent` attribute', async () => {
|
||||
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'));
|
||||
});
|
||||
|
|
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) {
|
||||
const args = [
|
||||
'queueEncryptedEnvelope error handling envelope',
|
||||
this.getEnvelopeId(envelope),
|
||||
this.getEnvelopeId(error.envelope || envelope),
|
||||
':',
|
||||
error && error.extra ? JSON.stringify(error.extra) : '',
|
||||
error && error.stack ? error.stack : error,
|
||||
|
@ -1587,7 +1587,10 @@ class MessageReceiverInner extends EventTarget {
|
|||
});
|
||||
|
||||
// Avoid deadlocks by scheduling processing on decrypted queue
|
||||
this.addToQueue(() => this.dispatchAndWait(event), TaskType.Decrypted);
|
||||
this.addToQueue(
|
||||
async () => this.dispatchEvent(event),
|
||||
TaskType.Decrypted
|
||||
);
|
||||
} else {
|
||||
const envelopeId = this.getEnvelopeId(newEnvelope);
|
||||
window.log.error(
|
||||
|
@ -1803,39 +1806,98 @@ class MessageReceiverInner extends EventTarget {
|
|||
);
|
||||
assert(envelope.content, 'Should have `content` field');
|
||||
const result = await this.decrypt(stores, envelope, envelope.content);
|
||||
|
||||
if (!result.plaintext) {
|
||||
window.log.warn('decryptContentMessage: plaintext was falsey');
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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.
|
||||
// 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!
|
||||
try {
|
||||
const content = Proto.Content.decode(result.plaintext);
|
||||
|
||||
if (
|
||||
content.senderKeyDistributionMessage &&
|
||||
Bytes.isNotEmpty(content.senderKeyDistributionMessage)
|
||||
) {
|
||||
await this.handleSenderKeyDistributionMessage(
|
||||
envelope,
|
||||
stores,
|
||||
result.envelope,
|
||||
content.senderKeyDistributionMessage
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorString = error && error.stack ? error.stack : 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 (
|
||||
content.decryptionErrorMessage &&
|
||||
Bytes.isNotEmpty(content.decryptionErrorMessage)
|
||||
|
@ -1908,10 +1970,11 @@ class MessageReceiverInner extends EventTarget {
|
|||
senderDevice: request.deviceId(),
|
||||
sentAt: request.timestamp(),
|
||||
});
|
||||
await this.dispatchAndWait(event);
|
||||
await this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
async handleSenderKeyDistributionMessage(
|
||||
stores: LockedStores,
|
||||
envelope: ProcessedEnvelope,
|
||||
distributionMessage: Uint8Array
|
||||
): Promise<void> {
|
||||
|
@ -1941,12 +2004,15 @@ class MessageReceiverInner extends EventTarget {
|
|||
const senderKeyStore = new SenderKeys();
|
||||
const address = `${identifier}.${sourceDevice}`;
|
||||
|
||||
await window.textsecure.storage.protocol.enqueueSenderKeyJob(address, () =>
|
||||
processSenderKeyDistributionMessage(
|
||||
sender,
|
||||
senderKeyDistributionMessage,
|
||||
senderKeyStore
|
||||
)
|
||||
await window.textsecure.storage.protocol.enqueueSenderKeyJob(
|
||||
address,
|
||||
() =>
|
||||
processSenderKeyDistributionMessage(
|
||||
sender,
|
||||
senderKeyDistributionMessage,
|
||||
senderKeyStore
|
||||
),
|
||||
stores.zone
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1989,6 +2055,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
envelopeTimestamp: envelope.timestamp,
|
||||
source: envelope.source,
|
||||
sourceUuid: envelope.sourceUuid,
|
||||
sourceDevice: envelope.sourceDevice,
|
||||
},
|
||||
this.removeFromCache.bind(this, envelope)
|
||||
);
|
||||
|
|
|
@ -48,6 +48,11 @@ export const enum SenderCertificateMode {
|
|||
WithoutE164,
|
||||
}
|
||||
|
||||
export type SendLogCallbackType = (options: {
|
||||
identifier: string;
|
||||
deviceIds: Array<number>;
|
||||
}) => Promise<void>;
|
||||
|
||||
type SendMetadata = {
|
||||
type: number;
|
||||
destinationDeviceId: number;
|
||||
|
@ -123,11 +128,11 @@ export default class OutgoingMessage {
|
|||
|
||||
errors: Array<CustomError>;
|
||||
|
||||
successfulIdentifiers: Array<unknown>;
|
||||
successfulIdentifiers: Array<string>;
|
||||
|
||||
failoverIdentifiers: Array<unknown>;
|
||||
failoverIdentifiers: Array<string>;
|
||||
|
||||
unidentifiedDeliveries: Array<unknown>;
|
||||
unidentifiedDeliveries: Array<string>;
|
||||
|
||||
sendMetadata?: SendMetadataType;
|
||||
|
||||
|
@ -137,16 +142,31 @@ export default class OutgoingMessage {
|
|||
|
||||
contentHint: number;
|
||||
|
||||
constructor(
|
||||
server: WebAPIType,
|
||||
timestamp: number,
|
||||
identifiers: Array<string>,
|
||||
message: Proto.Content | Proto.DataMessage | PlaintextContent,
|
||||
contentHint: number,
|
||||
groupId: string | undefined,
|
||||
callback: (result: CallbackResultType) => void,
|
||||
options: OutgoingMessageOptionsType = {}
|
||||
) {
|
||||
recipients: Record<string, Array<number>>;
|
||||
|
||||
sendLogCallback?: SendLogCallbackType;
|
||||
|
||||
constructor({
|
||||
callback,
|
||||
contentHint,
|
||||
groupId,
|
||||
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) {
|
||||
const content = new Proto.Content();
|
||||
content.dataMessage = message;
|
||||
|
@ -168,20 +188,29 @@ export default class OutgoingMessage {
|
|||
this.successfulIdentifiers = [];
|
||||
this.failoverIdentifiers = [];
|
||||
this.unidentifiedDeliveries = [];
|
||||
this.recipients = {};
|
||||
this.sendLogCallback = sendLogCallback;
|
||||
|
||||
const { sendMetadata, online } = options;
|
||||
this.sendMetadata = sendMetadata;
|
||||
this.online = online;
|
||||
this.sendMetadata = options?.sendMetadata;
|
||||
this.online = options?.online;
|
||||
}
|
||||
|
||||
numberCompleted(): void {
|
||||
this.identifiersCompleted += 1;
|
||||
if (this.identifiersCompleted >= this.identifiers.length) {
|
||||
const contentProto = this.getContentProtoBytes();
|
||||
const { timestamp, contentHint, recipients } = this;
|
||||
|
||||
this.callback({
|
||||
successfulIdentifiers: this.successfulIdentifiers,
|
||||
failoverIdentifiers: this.failoverIdentifiers,
|
||||
errors: this.errors,
|
||||
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
||||
|
||||
contentHint,
|
||||
recipients,
|
||||
contentProto,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -313,6 +342,14 @@ export default class OutgoingMessage {
|
|||
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({
|
||||
identityKeyStore,
|
||||
protocolAddress,
|
||||
|
@ -455,9 +492,21 @@ export default class OutgoingMessage {
|
|||
accessKey,
|
||||
}).then(
|
||||
() => {
|
||||
this.recipients[identifier] = deviceIds;
|
||||
this.unidentifiedDeliveries.push(identifier);
|
||||
this.successfulIdentifiers.push(identifier);
|
||||
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) => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
|
@ -481,7 +530,19 @@ export default class OutgoingMessage {
|
|||
return this.transmitMessage(identifier, jsonData, this.timestamp).then(
|
||||
() => {
|
||||
this.successfulIdentifiers.push(identifier);
|
||||
this.recipients[identifier] = deviceIds;
|
||||
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,
|
||||
} from './WebAPI';
|
||||
import createTaskWithTimeout from './TaskWithTimeout';
|
||||
import OutgoingMessage, { SerializedCertificateType } from './OutgoingMessage';
|
||||
import OutgoingMessage, {
|
||||
SerializedCertificateType,
|
||||
SendLogCallbackType,
|
||||
} from './OutgoingMessage';
|
||||
import Crypto from './Crypto';
|
||||
import * as Bytes from '../Bytes';
|
||||
import {
|
||||
|
@ -48,6 +51,11 @@ import {
|
|||
LinkPreviewMetadata,
|
||||
} from '../linkPreviews/linkPreviewFetch';
|
||||
import { concat } from '../util/iterables';
|
||||
import {
|
||||
handleMessageSend,
|
||||
shouldSaveProto,
|
||||
SendTypesType,
|
||||
} from '../util/handleMessageSend';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
export type SendMetadataType = {
|
||||
|
@ -68,11 +76,17 @@ export type CustomError = Error & {
|
|||
};
|
||||
|
||||
export type CallbackResultType = {
|
||||
successfulIdentifiers?: Array<any>;
|
||||
failoverIdentifiers?: Array<any>;
|
||||
successfulIdentifiers?: Array<string>;
|
||||
failoverIdentifiers?: Array<string>;
|
||||
errors?: Array<CustomError>;
|
||||
unidentifiedDeliveries?: Array<any>;
|
||||
unidentifiedDeliveries?: Array<string>;
|
||||
dataMessage?: ArrayBuffer;
|
||||
|
||||
// Fields necesary for send log save
|
||||
contentHint?: number;
|
||||
contentProto?: Uint8Array;
|
||||
timestamp?: number;
|
||||
recipients?: Record<string, Array<number>>;
|
||||
};
|
||||
|
||||
type PreviewType = {
|
||||
|
@ -593,9 +607,12 @@ export default class MessageSender {
|
|||
try {
|
||||
const { sticker } = message;
|
||||
|
||||
if (!sticker || !sticker.data) {
|
||||
if (!sticker) {
|
||||
return;
|
||||
}
|
||||
if (!sticker.data) {
|
||||
throw new Error('uploadSticker: No sticker data to upload!');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.sticker = {
|
||||
|
@ -824,21 +841,23 @@ export default class MessageSender {
|
|||
}
|
||||
|
||||
sendMessageProto({
|
||||
timestamp,
|
||||
recipients,
|
||||
proto,
|
||||
callback,
|
||||
contentHint,
|
||||
groupId,
|
||||
callback,
|
||||
options,
|
||||
proto,
|
||||
recipients,
|
||||
sendLogCallback,
|
||||
timestamp,
|
||||
}: {
|
||||
timestamp: number;
|
||||
recipients: Array<string>;
|
||||
proto: Proto.Content | Proto.DataMessage | PlaintextContent;
|
||||
callback: (result: CallbackResultType) => void;
|
||||
contentHint: number;
|
||||
groupId: string | undefined;
|
||||
callback: (result: CallbackResultType) => void;
|
||||
options?: SendOptionsType;
|
||||
proto: Proto.Content | Proto.DataMessage | PlaintextContent;
|
||||
recipients: Array<string>;
|
||||
sendLogCallback?: SendLogCallbackType;
|
||||
timestamp: number;
|
||||
}): void {
|
||||
const rejections = window.textsecure.storage.get(
|
||||
'signedKeyRotationRejected',
|
||||
|
@ -848,16 +867,17 @@ export default class MessageSender {
|
|||
throw new SignedPreKeyRotationError();
|
||||
}
|
||||
|
||||
const outgoing = new OutgoingMessage(
|
||||
this.server,
|
||||
timestamp,
|
||||
recipients,
|
||||
proto,
|
||||
const outgoing = new OutgoingMessage({
|
||||
callback,
|
||||
contentHint,
|
||||
groupId,
|
||||
callback,
|
||||
options
|
||||
);
|
||||
identifiers: recipients,
|
||||
message: proto,
|
||||
options,
|
||||
sendLogCallback,
|
||||
server: this.server,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
recipients.forEach(identifier => {
|
||||
this.queueJobForIdentifier(identifier, async () =>
|
||||
|
@ -992,6 +1012,8 @@ export default class MessageSender {
|
|||
|
||||
// Support for sync messages
|
||||
|
||||
// Note: this is used for sending real messages to your other devices after sending a
|
||||
// message to others.
|
||||
async sendSyncMessage({
|
||||
encodedDataMessage,
|
||||
timestamp,
|
||||
|
@ -1012,14 +1034,9 @@ export default class MessageSender {
|
|||
unidentifiedDeliveries?: Array<string>;
|
||||
isUpdate?: boolean;
|
||||
options?: SendOptionsType;
|
||||
}): Promise<CallbackResultType | void> {
|
||||
}): Promise<CallbackResultType> {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
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(
|
||||
new FIXMEU8(encodedDataMessage)
|
||||
|
@ -1082,134 +1099,112 @@ export default class MessageSender {
|
|||
identifier: myUuid || myNumber,
|
||||
proto: contentMessage,
|
||||
timestamp,
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
async sendRequestBlockSyncMessage(
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType | void> {
|
||||
): Promise<CallbackResultType> {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
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({
|
||||
identifier: myUuid || myNumber,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
options,
|
||||
});
|
||||
}
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return Promise.resolve();
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
async sendRequestConfigurationSyncMessage(
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType | void> {
|
||||
): Promise<CallbackResultType> {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
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({
|
||||
identifier: myUuid || myNumber,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
options,
|
||||
});
|
||||
}
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return Promise.resolve();
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
async sendRequestGroupSyncMessage(
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType | void> {
|
||||
): Promise<CallbackResultType> {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
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({
|
||||
identifier: myUuid || myNumber,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
options,
|
||||
});
|
||||
}
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return Promise.resolve();
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
async sendRequestContactSyncMessage(
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType | void> {
|
||||
): Promise<CallbackResultType> {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
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.CONTACTS;
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.request = request;
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
request.type = Proto.SyncMessage.Request.Type.CONTACTS;
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.request = request;
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto({
|
||||
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 sendFetchManifestSyncMessage(
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType | void> {
|
||||
): Promise<CallbackResultType> {
|
||||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
|
||||
if (myDevice === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchLatest = new Proto.SyncMessage.FetchLatest();
|
||||
fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST;
|
||||
|
@ -1221,7 +1216,7 @@ export default class MessageSender {
|
|||
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await this.sendIndividualProto({
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
|
@ -1232,14 +1227,9 @@ export default class MessageSender {
|
|||
|
||||
async sendRequestKeySyncMessage(
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType | void> {
|
||||
): Promise<CallbackResultType> {
|
||||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
|
||||
if (myDevice === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
request.type = Proto.SyncMessage.Request.Type.KEYS;
|
||||
|
@ -1251,7 +1241,7 @@ export default class MessageSender {
|
|||
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await this.sendIndividualProto({
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
|
@ -1267,13 +1257,10 @@ export default class MessageSender {
|
|||
timestamp: number;
|
||||
}>,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType | void> {
|
||||
): Promise<CallbackResultType> {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
if (myDevice === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.read = [];
|
||||
for (let i = 0; i < reads.length; i += 1) {
|
||||
|
@ -1290,7 +1277,7 @@ export default class MessageSender {
|
|||
identifier: myUuid || myNumber,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
@ -1300,13 +1287,9 @@ export default class MessageSender {
|
|||
senderUuid: string,
|
||||
timestamp: number,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType | null> {
|
||||
): Promise<CallbackResultType> {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
if (myDevice === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
|
||||
|
@ -1327,7 +1310,7 @@ export default class MessageSender {
|
|||
identifier: myUuid || myNumber,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
@ -1340,13 +1323,9 @@ export default class MessageSender {
|
|||
type: number;
|
||||
},
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType | null> {
|
||||
): Promise<CallbackResultType> {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
if (myDevice === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
|
||||
|
@ -1372,7 +1351,7 @@ export default class MessageSender {
|
|||
identifier: myUuid || myNumber,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
@ -1384,12 +1363,7 @@ export default class MessageSender {
|
|||
installed: boolean;
|
||||
}>,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType | null> {
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
if (myDevice === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
): Promise<CallbackResultType> {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const ENUM = Proto.SyncMessage.StickerPackOperation.Type;
|
||||
|
@ -1423,57 +1397,60 @@ export default class MessageSender {
|
|||
}
|
||||
|
||||
async syncVerification(
|
||||
destinationE164: string,
|
||||
destinationUuid: string,
|
||||
destinationE164: string | undefined,
|
||||
destinationUuid: string | undefined,
|
||||
state: number,
|
||||
identityKey: ArrayBuffer,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType | void> {
|
||||
): Promise<CallbackResultType> {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
const now = Date.now();
|
||||
|
||||
if (myDevice === 1) {
|
||||
return Promise.resolve();
|
||||
if (!destinationE164 && !destinationUuid) {
|
||||
throw new Error('syncVerification: Neither e164 nor UUID were provided');
|
||||
}
|
||||
|
||||
// Get padding which we can share between null message and verified sync
|
||||
const padding = this.getRandomPadding();
|
||||
|
||||
// First send a null message to mask the sync message.
|
||||
const promise = this.sendNullMessage(
|
||||
{ uuid: destinationUuid, e164: destinationE164, padding },
|
||||
options
|
||||
await handleMessageSend(
|
||||
this.sendNullMessage(
|
||||
{ uuid: destinationUuid, e164: destinationE164, padding },
|
||||
options
|
||||
),
|
||||
{
|
||||
messageIds: [],
|
||||
sendType: 'nullMessage',
|
||||
}
|
||||
);
|
||||
|
||||
return promise.then(async () => {
|
||||
const verified = new Proto.Verified();
|
||||
verified.state = state;
|
||||
if (destinationE164) {
|
||||
verified.destination = destinationE164;
|
||||
}
|
||||
if (destinationUuid) {
|
||||
verified.destinationUuid = destinationUuid;
|
||||
}
|
||||
verified.identityKey = new FIXMEU8(identityKey);
|
||||
verified.nullMessage = padding;
|
||||
const verified = new Proto.Verified();
|
||||
verified.state = state;
|
||||
if (destinationE164) {
|
||||
verified.destination = destinationE164;
|
||||
}
|
||||
if (destinationUuid) {
|
||||
verified.destinationUuid = destinationUuid;
|
||||
}
|
||||
verified.identityKey = new FIXMEU8(identityKey);
|
||||
verified.nullMessage = padding;
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.verified = verified;
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.verified = verified;
|
||||
|
||||
const secondMessage = new Proto.Content();
|
||||
secondMessage.syncMessage = syncMessage;
|
||||
const secondMessage = new Proto.Content();
|
||||
secondMessage.syncMessage = syncMessage;
|
||||
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
proto: secondMessage,
|
||||
timestamp: now,
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
options,
|
||||
});
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
proto: secondMessage,
|
||||
timestamp: now,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1512,7 +1489,7 @@ export default class MessageSender {
|
|||
recipientId: string,
|
||||
callingMessage: Proto.ICallingMessage,
|
||||
options?: SendOptionsType
|
||||
): Promise<void> {
|
||||
): Promise<CallbackResultType> {
|
||||
const recipients = [recipientId];
|
||||
const finalTimestamp = Date.now();
|
||||
|
||||
|
@ -1521,7 +1498,7 @@ export default class MessageSender {
|
|||
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await this.sendMessageProtoAndWait({
|
||||
return this.sendMessageProtoAndWait({
|
||||
timestamp: finalTimestamp,
|
||||
recipients,
|
||||
proto: contentMessage,
|
||||
|
@ -1537,16 +1514,15 @@ export default class MessageSender {
|
|||
timestamps,
|
||||
options,
|
||||
}: {
|
||||
e164: string;
|
||||
uuid: string;
|
||||
e164?: string;
|
||||
uuid?: string;
|
||||
timestamps: Array<number>;
|
||||
options?: SendOptionsType;
|
||||
}): Promise<CallbackResultType | void> {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
if ((myNumber === e164 || myUuid === uuid) && myDevice === 1) {
|
||||
return Promise.resolve();
|
||||
}): Promise<CallbackResultType> {
|
||||
if (!uuid && !e164) {
|
||||
throw new Error(
|
||||
'sendDeliveryReceipt: Neither uuid nor e164 was provided!'
|
||||
);
|
||||
}
|
||||
|
||||
const receiptMessage = new Proto.ReceiptMessage();
|
||||
|
@ -1562,7 +1538,7 @@ export default class MessageSender {
|
|||
identifier: uuid || e164,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
@ -1591,7 +1567,7 @@ export default class MessageSender {
|
|||
identifier: senderUuid || senderE164,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
@ -1634,9 +1610,7 @@ export default class MessageSender {
|
|||
e164: string,
|
||||
timestamp: number,
|
||||
options?: SendOptionsType
|
||||
): Promise<
|
||||
CallbackResultType | void | Array<CallbackResultType | void | Array<void>>
|
||||
> {
|
||||
): Promise<CallbackResultType> {
|
||||
window.log.info('resetSession: start');
|
||||
const proto = new Proto.DataMessage();
|
||||
proto.body = 'TERMINATE';
|
||||
|
@ -1659,19 +1633,27 @@ export default class MessageSender {
|
|||
window.log.info(
|
||||
'resetSession: finished closing local sessions, now sending to contact'
|
||||
);
|
||||
return this.sendIndividualProto({
|
||||
identifier,
|
||||
proto,
|
||||
timestamp,
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
options,
|
||||
}).catch(logError('resetSession/sendToContact error:'));
|
||||
return handleMessageSend(
|
||||
this.sendIndividualProto({
|
||||
identifier,
|
||||
proto,
|
||||
timestamp,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
options,
|
||||
}),
|
||||
{
|
||||
messageIds: [],
|
||||
sendType: 'resetSession',
|
||||
}
|
||||
).catch(logError('resetSession/sendToContact error:'));
|
||||
})
|
||||
.then(async () =>
|
||||
window.textsecure.storage.protocol
|
||||
.then(async result => {
|
||||
await window.textsecure.storage.protocol
|
||||
.archiveAllSessions(identifier)
|
||||
.catch(logError('resetSession/archiveAllSessions2 error:'))
|
||||
);
|
||||
.catch(logError('resetSession/archiveAllSessions2 error:'));
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
|
@ -1694,7 +1676,12 @@ export default class MessageSender {
|
|||
options,
|
||||
}).catch(logError('resetSession/sendSync error:'));
|
||||
|
||||
return Promise.all([sendToContactPromise, sendSyncPromise]);
|
||||
const responses = await Promise.all([
|
||||
sendToContactPromise,
|
||||
sendSyncPromise,
|
||||
]);
|
||||
|
||||
return responses[0];
|
||||
}
|
||||
|
||||
async sendExpirationTimerUpdateToIdentifier(
|
||||
|
@ -1714,17 +1701,19 @@ export default class MessageSender {
|
|||
profileKey,
|
||||
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
},
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
groupId: undefined,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
async sendRetryRequest({
|
||||
groupId,
|
||||
options,
|
||||
plaintext,
|
||||
uuid,
|
||||
}: {
|
||||
groupId?: string;
|
||||
options?: SendOptionsType;
|
||||
plaintext: PlaintextContent;
|
||||
uuid: string;
|
||||
|
@ -1735,29 +1724,99 @@ export default class MessageSender {
|
|||
timestamp: Date.now(),
|
||||
recipients: [uuid],
|
||||
proto: plaintext,
|
||||
contentHint: ContentHint.IMPLICIT,
|
||||
groupId: undefined,
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
groupId,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
async sendGroupProto({
|
||||
recipients,
|
||||
proto,
|
||||
timestamp = Date.now(),
|
||||
contentHint,
|
||||
groupId,
|
||||
options,
|
||||
proto,
|
||||
recipients,
|
||||
sendLogCallback,
|
||||
timestamp = Date.now(),
|
||||
}: {
|
||||
recipients: Array<string>;
|
||||
proto: Proto.Content;
|
||||
timestamp: number;
|
||||
contentHint: number;
|
||||
groupId: string | undefined;
|
||||
options?: SendOptionsType;
|
||||
proto: Proto.Content;
|
||||
recipients: Array<string>;
|
||||
sendLogCallback?: SendLogCallbackType;
|
||||
timestamp: number;
|
||||
}): Promise<CallbackResultType> {
|
||||
const dataMessage = proto.dataMessage
|
||||
? typedArrayToArrayBuffer(
|
||||
|
@ -1790,13 +1849,14 @@ export default class MessageSender {
|
|||
};
|
||||
|
||||
this.sendMessageProto({
|
||||
timestamp,
|
||||
recipients: identifiers,
|
||||
proto,
|
||||
callback,
|
||||
contentHint,
|
||||
groupId,
|
||||
callback,
|
||||
options,
|
||||
proto,
|
||||
recipients: identifiers,
|
||||
sendLogCallback,
|
||||
timestamp,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1846,19 +1906,31 @@ export default class MessageSender {
|
|||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
const contentMessage = new Proto.Content();
|
||||
const timestamp = Date.now();
|
||||
|
||||
const senderKeyDistributionMessage = await this.getSenderKeyDistributionMessage(
|
||||
distributionId
|
||||
);
|
||||
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({
|
||||
recipients: identifiers,
|
||||
proto: contentMessage,
|
||||
timestamp: Date.now(),
|
||||
contentHint,
|
||||
groupId,
|
||||
options,
|
||||
proto: contentMessage,
|
||||
recipients: identifiers,
|
||||
sendLogCallback,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1869,6 +1941,7 @@ export default class MessageSender {
|
|||
groupIdentifiers: Array<string>,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
const timestamp = Date.now();
|
||||
const proto = new Proto.Content({
|
||||
dataMessage: {
|
||||
group: {
|
||||
|
@ -1879,13 +1952,26 @@ export default class MessageSender {
|
|||
});
|
||||
|
||||
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({
|
||||
recipients: groupIdentifiers,
|
||||
proto,
|
||||
timestamp: Date.now(),
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
contentHint,
|
||||
groupId: undefined, // only for GV2 ids
|
||||
options,
|
||||
proto,
|
||||
recipients: groupIdentifiers,
|
||||
sendLogCallback,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1913,6 +1999,7 @@ export default class MessageSender {
|
|||
type: Proto.GroupContext.Type.DELIVER,
|
||||
},
|
||||
};
|
||||
const proto = await this.getContentMessage(messageOptions);
|
||||
|
||||
if (recipients.length === 0) {
|
||||
return Promise.resolve({
|
||||
|
@ -1925,11 +2012,25 @@ export default class MessageSender {
|
|||
}
|
||||
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
return this.sendMessage({
|
||||
messageOptions,
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
const contentHint = ContentHint.RESENDABLE;
|
||||
const sendLogCallback =
|
||||
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
|
||||
options,
|
||||
proto,
|
||||
recipients,
|
||||
sendLogCallback,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ import MessageReceiver from './MessageReceiver';
|
|||
import { ContactSyncEvent, GroupSyncEvent } from './messageReceiverEvents';
|
||||
import MessageSender from './SendMessage';
|
||||
import { assert } from '../util/assert';
|
||||
import { getSendOptions } from '../util/getSendOptions';
|
||||
import { handleMessageSend } from '../util/handleMessageSend';
|
||||
|
||||
class SyncRequestInner extends EventTarget {
|
||||
private started = false;
|
||||
|
@ -61,25 +63,41 @@ class SyncRequestInner extends EventTarget {
|
|||
|
||||
const { sender } = this;
|
||||
|
||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = await window.ConversationController.prepareForSend(ourNumber, {
|
||||
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||
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...');
|
||||
wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));
|
||||
handleMessageSend(sender.sendRequestConfigurationSyncMessage(sendOptions), {
|
||||
messageIds: [],
|
||||
sendType: 'otherSync',
|
||||
});
|
||||
|
||||
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...');
|
||||
wrap(sender.sendRequestContactSyncMessage(sendOptions))
|
||||
handleMessageSend(sender.sendRequestContactSyncMessage(sendOptions), {
|
||||
messageIds: [],
|
||||
sendType: 'otherSync',
|
||||
})
|
||||
.then(() => {
|
||||
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) => {
|
||||
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;
|
||||
serverGuid: string;
|
||||
serverTimestamp: number;
|
||||
groupId?: string;
|
||||
}>;
|
||||
|
||||
export type ProcessedAttachment = {
|
||||
|
|
|
@ -219,6 +219,7 @@ export type ReadEventData = Readonly<{
|
|||
envelopeTimestamp: number;
|
||||
source?: string;
|
||||
sourceUuid?: string;
|
||||
sourceDevice?: number;
|
||||
}>;
|
||||
|
||||
export class ReadEvent extends ConfirmableEvent {
|
||||
|
|
|
@ -5,6 +5,7 @@ export type ReactionType = Readonly<{
|
|||
conversationId: string;
|
||||
emoji: string;
|
||||
fromId: string;
|
||||
messageId: string | undefined;
|
||||
messageReceivedAt: number;
|
||||
targetAuthorUuid: string;
|
||||
targetTimestamp: number;
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isNumber } from 'lodash';
|
||||
import { CallbackResultType } from '../textsecure/SendMessage';
|
||||
import dataInterface from '../sql/Client';
|
||||
|
||||
const { insertSentProto } = dataInterface;
|
||||
|
||||
export const SEALED_SENDER = {
|
||||
UNKNOWN: 0,
|
||||
|
@ -10,17 +14,71 @@ export const SEALED_SENDER = {
|
|||
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(
|
||||
promise: Promise<CallbackResultType | void | null>
|
||||
): Promise<CallbackResultType | void | null> {
|
||||
promise: Promise<CallbackResultType>,
|
||||
options: {
|
||||
messageIds: Array<string>;
|
||||
sendType: SendTypesType;
|
||||
}
|
||||
): Promise<CallbackResultType> {
|
||||
try {
|
||||
const result = await promise;
|
||||
if (result) {
|
||||
await handleMessageSendResult(
|
||||
result.failoverIdentifiers,
|
||||
result.unidentifiedDeliveries
|
||||
);
|
||||
}
|
||||
|
||||
await maybeSaveToSendLog(result, options);
|
||||
|
||||
await handleMessageSendResult(
|
||||
result.failoverIdentifiers,
|
||||
result.unidentifiedDeliveries
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (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",
|
||||
"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(",
|
||||
"path": "ts/types/Stickers.js",
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
|
||||
import { ConversationAttributesType } from '../model-types.d';
|
||||
import { handleMessageSend } from './handleMessageSend';
|
||||
import { getSendOptions } from './getSendOptions';
|
||||
import { sendReadReceiptsFor } from './sendReadReceiptsFor';
|
||||
import { hasErrors } from '../state/selectors/message';
|
||||
import { isNotNil } from './isNotNil';
|
||||
|
||||
export async function markConversationRead(
|
||||
conversationAttrs: ConversationAttributesType,
|
||||
|
@ -43,6 +45,7 @@ export async function markConversationRead(
|
|||
const unreadReactionSyncData = new Map<
|
||||
string,
|
||||
{
|
||||
messageId?: string;
|
||||
senderUuid?: string;
|
||||
senderE164?: string;
|
||||
timestamp: number;
|
||||
|
@ -54,6 +57,7 @@ export async function markConversationRead(
|
|||
return;
|
||||
}
|
||||
unreadReactionSyncData.set(targetKey, {
|
||||
messageId: reaction.messageId,
|
||||
senderE164: undefined,
|
||||
senderUuid: reaction.targetAuthorUuid,
|
||||
timestamp: reaction.targetTimestamp,
|
||||
|
@ -68,6 +72,7 @@ export async function markConversationRead(
|
|||
}
|
||||
|
||||
return {
|
||||
messageId: messageSyncData.id,
|
||||
senderE164: messageSyncData.source,
|
||||
senderUuid: messageSyncData.sourceUuid,
|
||||
senderId: window.ConversationController.ensureContactIds({
|
||||
|
@ -89,25 +94,39 @@ export async function markConversationRead(
|
|||
item => Boolean(item.senderId) && !item.hasErrors
|
||||
);
|
||||
|
||||
const readSyncs = [
|
||||
const readSyncs: Array<{
|
||||
messageId?: string;
|
||||
senderE164?: string;
|
||||
senderUuid?: string;
|
||||
senderId?: string;
|
||||
timestamp: number;
|
||||
hasErrors?: string;
|
||||
}> = [
|
||||
...unreadMessagesSyncData,
|
||||
...Array.from(unreadReactionSyncData.values()),
|
||||
];
|
||||
const messageIds = readSyncs.map(item => item.messageId).filter(isNotNil);
|
||||
|
||||
if (readSyncs.length && options.sendReadReceipts) {
|
||||
window.log.info(`Sending ${readSyncs.length} read syncs`);
|
||||
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
|
||||
// to a contact, we need accessKeys for both.
|
||||
const {
|
||||
sendOptions,
|
||||
} = await window.ConversationController.prepareForSend(
|
||||
window.ConversationController.getOurConversationId(),
|
||||
{ syncMessage: true }
|
||||
);
|
||||
const ourConversation = window.ConversationController.getOurConversationOrThrow();
|
||||
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -63,14 +63,14 @@ export class RetryPlaceholders {
|
|||
}
|
||||
|
||||
this.items = parsed.success ? parsed.data : [];
|
||||
window.log.info(
|
||||
`RetryPlaceholders.constructor: Started with ${this.items.length} items`
|
||||
);
|
||||
|
||||
this.sortByExpiresAtAsc();
|
||||
this.byConversation = this.makeByConversationLookup();
|
||||
this.byMessage = this.makeByMessageLookup();
|
||||
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
|
||||
|
|
|
@ -7,9 +7,18 @@ import { getSendOptions } from './getSendOptions';
|
|||
import { handleMessageSend } from './handleMessageSend';
|
||||
import { isConversationAccepted } from './isConversationAccepted';
|
||||
|
||||
type ReceiptSpecType = {
|
||||
messageId: string;
|
||||
senderE164?: string;
|
||||
senderUuid?: string;
|
||||
senderId?: string;
|
||||
timestamp: number;
|
||||
hasErrors: boolean;
|
||||
};
|
||||
|
||||
export async function sendReadReceiptsFor(
|
||||
conversationAttrs: ConversationAttributesType,
|
||||
items: Array<unknown>
|
||||
items: Array<ReceiptSpecType>
|
||||
): Promise<void> {
|
||||
// Only send read receipts for accepted conversations
|
||||
if (
|
||||
|
@ -22,7 +31,8 @@ export async function sendReadReceiptsFor(
|
|||
|
||||
await Promise.all(
|
||||
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);
|
||||
|
||||
if (conversation) {
|
||||
|
@ -34,7 +44,8 @@ export async function sendReadReceiptsFor(
|
|||
senderUuid: conversation.get('uuid')!,
|
||||
timestamps,
|
||||
options: sendOptions,
|
||||
})
|
||||
}),
|
||||
{ messageIds, sendType: 'readReceipt' }
|
||||
);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { differenceWith, partition } from 'lodash';
|
||||
import { differenceWith, omit, partition } from 'lodash';
|
||||
import PQueue from 'p-queue';
|
||||
|
||||
import {
|
||||
|
@ -16,6 +16,7 @@ import { senderCertificateService } from '../services/senderCertificate';
|
|||
import {
|
||||
padMessage,
|
||||
SenderCertificateMode,
|
||||
SendLogCallbackType,
|
||||
} from '../textsecure/OutgoingMessage';
|
||||
import { isEnabled } from '../RemoteConfig';
|
||||
|
||||
|
@ -30,7 +31,12 @@ import { ConversationModel } from '../models/conversations';
|
|||
import { DeviceType } from '../textsecure/Types.d';
|
||||
import { getKeysForIdentifier } from '../textsecure/getKeysForIdentifier';
|
||||
import { ConversationAttributesType } from '../model-types.d';
|
||||
import { SEALED_SENDER } from './handleMessageSend';
|
||||
import {
|
||||
handleMessageSend,
|
||||
SEALED_SENDER,
|
||||
SendTypesType,
|
||||
shouldSaveProto,
|
||||
} from './handleMessageSend';
|
||||
import { parseIntOrThrow } from './parseIntOrThrow';
|
||||
import {
|
||||
multiRecipient200ResponseSchema,
|
||||
|
@ -59,17 +65,21 @@ const FIXMEU8 = Uint8Array;
|
|||
// Public API:
|
||||
|
||||
export async function sendToGroup({
|
||||
groupSendOptions,
|
||||
conversation,
|
||||
contentHint,
|
||||
sendOptions,
|
||||
conversation,
|
||||
groupSendOptions,
|
||||
messageId,
|
||||
isPartialSend,
|
||||
sendOptions,
|
||||
sendType,
|
||||
}: {
|
||||
groupSendOptions: GroupSendOptionsType;
|
||||
conversation: ConversationModel;
|
||||
contentHint: number;
|
||||
sendOptions?: SendOptionsType;
|
||||
conversation: ConversationModel;
|
||||
groupSendOptions: GroupSendOptionsType;
|
||||
isPartialSend?: boolean;
|
||||
messageId: string | undefined;
|
||||
sendOptions?: SendOptionsType;
|
||||
sendType: SendTypesType;
|
||||
}): Promise<CallbackResultType> {
|
||||
assert(
|
||||
window.textsecure.messaging,
|
||||
|
@ -92,8 +102,10 @@ export async function sendToGroup({
|
|||
contentMessage,
|
||||
conversation,
|
||||
isPartialSend,
|
||||
messageId,
|
||||
recipients,
|
||||
sendOptions,
|
||||
sendType,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
@ -103,18 +115,22 @@ export async function sendContentMessageToGroup({
|
|||
contentMessage,
|
||||
conversation,
|
||||
isPartialSend,
|
||||
messageId,
|
||||
online,
|
||||
recipients,
|
||||
sendOptions,
|
||||
sendType,
|
||||
timestamp,
|
||||
}: {
|
||||
contentHint: number;
|
||||
contentMessage: Proto.Content;
|
||||
conversation: ConversationModel;
|
||||
isPartialSend?: boolean;
|
||||
messageId: string | undefined;
|
||||
online?: boolean;
|
||||
recipients: Array<string>;
|
||||
sendOptions?: SendOptionsType;
|
||||
sendType: SendTypesType;
|
||||
timestamp: number;
|
||||
}): Promise<CallbackResultType> {
|
||||
const logId = conversation.idForLogging();
|
||||
|
@ -127,7 +143,7 @@ export async function sendContentMessageToGroup({
|
|||
const ourConversation = window.ConversationController.get(ourConversationId);
|
||||
|
||||
if (
|
||||
isEnabled('desktop.sendSenderKey') &&
|
||||
isEnabled('desktop.sendSenderKey2') &&
|
||||
ourConversation?.get('capabilities')?.senderKey &&
|
||||
isGroupV2(conversation.attributes)
|
||||
) {
|
||||
|
@ -137,10 +153,12 @@ export async function sendContentMessageToGroup({
|
|||
contentMessage,
|
||||
conversation,
|
||||
isPartialSend,
|
||||
messageId,
|
||||
online,
|
||||
recipients,
|
||||
recursionCount: 0,
|
||||
sendOptions,
|
||||
sendType,
|
||||
timestamp,
|
||||
});
|
||||
} 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)
|
||||
? conversation.get('groupId')
|
||||
: undefined;
|
||||
return window.textsecure.messaging.sendGroupProto({
|
||||
recipients,
|
||||
proto: contentMessage,
|
||||
timestamp,
|
||||
contentHint,
|
||||
groupId,
|
||||
options: { ...sendOptions, online },
|
||||
proto: contentMessage,
|
||||
recipients,
|
||||
sendLogCallback,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -171,10 +197,12 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
contentMessage: Proto.Content;
|
||||
conversation: ConversationModel;
|
||||
isPartialSend?: boolean;
|
||||
messageId: string | undefined;
|
||||
online?: boolean;
|
||||
recipients: Array<string>;
|
||||
recursionCount: number;
|
||||
sendOptions?: SendOptionsType;
|
||||
sendType: SendTypesType;
|
||||
timestamp: number;
|
||||
}): Promise<CallbackResultType> {
|
||||
const {
|
||||
|
@ -182,10 +210,12 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
contentMessage,
|
||||
conversation,
|
||||
isPartialSend,
|
||||
messageId,
|
||||
online,
|
||||
recursionCount,
|
||||
recipients,
|
||||
sendOptions,
|
||||
sendType,
|
||||
timestamp,
|
||||
} = options;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
@ -287,12 +317,16 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
currentDevices,
|
||||
device => isValidSenderKeyRecipient(conversation, device.identifier)
|
||||
);
|
||||
|
||||
const senderKeyRecipients = getUuidsFromDevices(devicesForSenderKey);
|
||||
const normalSendRecipients = getUuidsFromDevices(devicesForNormalSend);
|
||||
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
|
||||
const senderKeyRecipients = getUuidsFromDevices(devicesForSenderKey);
|
||||
if (senderKeyRecipients.length < 2) {
|
||||
throw new Error(
|
||||
`sendToGroupViaSenderKey/${logId}: Not enough recipients for Sender Key message. Failing over.`
|
||||
|
@ -335,14 +369,17 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
newToMemberUuids.length
|
||||
} members: ${JSON.stringify(newToMemberUuids)}`
|
||||
);
|
||||
await window.textsecure.messaging.sendSenderKeyDistributionMessage(
|
||||
{
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
distributionId,
|
||||
groupId,
|
||||
identifiers: newToMemberUuids,
|
||||
},
|
||||
sendOptions
|
||||
await handleMessageSend(
|
||||
window.textsecure.messaging.sendSenderKeyDistributionMessage(
|
||||
{
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
distributionId,
|
||||
groupId,
|
||||
identifiers: newToMemberUuids,
|
||||
},
|
||||
sendOptions
|
||||
),
|
||||
{ messageIds: [], sendType: 'senderKeyDistributionMessage' }
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -368,6 +405,14 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
const messageBuffer = await encryptForSenderKey({
|
||||
contentHint,
|
||||
|
@ -397,6 +442,11 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
),
|
||||
});
|
||||
}
|
||||
|
||||
senderKeyRecipientsWithDevices = omit(
|
||||
senderKeyRecipientsWithDevices,
|
||||
uuids404 || []
|
||||
);
|
||||
} else {
|
||||
window.log.error(
|
||||
`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) {
|
||||
if (error.code === ERROR_EXPIRED_OR_MISSING_DEVICES) {
|
||||
await handle409Response(logId, error);
|
||||
|
@ -426,13 +490,14 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
}
|
||||
|
||||
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
|
||||
const normalRecipients = getUuidsFromDevices(devicesForNormalSend);
|
||||
if (normalRecipients.length === 0) {
|
||||
if (normalSendRecipients.length === 0) {
|
||||
return {
|
||||
dataMessage: contentMessage.dataMessage
|
||||
? toArrayBuffer(
|
||||
|
@ -441,18 +506,59 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
: undefined,
|
||||
successfulIdentifiers: 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
|
||||
// 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({
|
||||
recipients: normalRecipients,
|
||||
proto: contentMessage,
|
||||
timestamp,
|
||||
contentHint,
|
||||
groupId,
|
||||
options: { ...sendOptions, online },
|
||||
proto: contentMessage,
|
||||
recipients: normalSendRecipients,
|
||||
sendLogCallback,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -471,6 +577,14 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
...(normalSendResult.unidentifiedDeliveries || []),
|
||||
...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({
|
||||
className: 'toast toast-clickable',
|
||||
initialize(options: any) {
|
||||
|
@ -2939,7 +2957,10 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
okText: window.i18n('delete'),
|
||||
resolve: async () => {
|
||||
try {
|
||||
await this.model.sendDeleteForEveryoneMessage(message.get('sent_at'));
|
||||
await this.model.sendDeleteForEveryoneMessage({
|
||||
id: message.id,
|
||||
timestamp: message.get('sent_at'),
|
||||
});
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Error sending delete-for-everyone',
|
||||
|
@ -3673,6 +3694,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
}
|
||||
|
||||
await this.model.sendReactionMessage(reaction, {
|
||||
messageId,
|
||||
targetAuthorUuid: messageModel.getSourceUuid(),
|
||||
targetTimestamp: messageModel.get('sent_at'),
|
||||
});
|
||||
|
|
20
ts/window.d.ts
vendored
20
ts/window.d.ts
vendored
|
@ -16,7 +16,6 @@ import {
|
|||
MessageModelCollectionType,
|
||||
MessageAttributesType,
|
||||
ReactionAttributesType,
|
||||
ReactionModelType,
|
||||
} from './model-types.d';
|
||||
import { TextSecureType } from './textsecure.d';
|
||||
import { Storage } from './textsecure/Storage';
|
||||
|
@ -241,6 +240,7 @@ declare global {
|
|||
showWindow: () => void;
|
||||
showSettings: () => void;
|
||||
shutdown: () => void;
|
||||
showDebugLog: () => void;
|
||||
sendChallengeRequest: (request: IPCChallengeRequest) => void;
|
||||
setAutoHideMenuBar: (value: WhatIsThis) => void;
|
||||
setBadgeCount: (count: number) => void;
|
||||
|
@ -290,6 +290,7 @@ declare global {
|
|||
onTimeout: (timestamp: number, cb: () => void, id?: string) => string;
|
||||
removeTimeout: (uuid: string) => void;
|
||||
retryPlaceholders?: Util.RetryPlaceholders;
|
||||
lightSessionResetQueue?: PQueue;
|
||||
runStorageServiceSyncJob: () => Promise<void>;
|
||||
storageServiceUploadJob: () => void;
|
||||
};
|
||||
|
@ -494,6 +495,7 @@ declare global {
|
|||
GV2_ENABLE_STATE_PROCESSING: boolean;
|
||||
GV2_MIGRATION_DISABLE_ADD: boolean;
|
||||
GV2_MIGRATION_DISABLE_INVITE: boolean;
|
||||
RETRY_DELAY: boolean;
|
||||
}
|
||||
|
||||
// We want to extend `Error`, so we need an interface.
|
||||
|
@ -536,6 +538,13 @@ export class CanvasVideoRenderer {
|
|||
constructor(canvas: Ref<HTMLCanvasElement>);
|
||||
}
|
||||
|
||||
export type DeliveryReceiptBatcherItemType = {
|
||||
messageId: string;
|
||||
source?: string;
|
||||
sourceUuid?: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type LoggerType = {
|
||||
fatal: LogFunctionType;
|
||||
info: LogFunctionType;
|
||||
|
@ -614,12 +623,8 @@ export type WhisperType = {
|
|||
ExpiringMessagesListener: WhatIsThis;
|
||||
TapToViewMessagesListener: WhatIsThis;
|
||||
|
||||
deliveryReceiptQueue: PQueue<WhatIsThis>;
|
||||
deliveryReceiptBatcher: BatcherType<{
|
||||
source?: string;
|
||||
sourceUuid?: string;
|
||||
timestamp: number;
|
||||
}>;
|
||||
deliveryReceiptQueue: PQueue;
|
||||
deliveryReceiptBatcher: BatcherType<DeliveryReceiptBatcherItemType>;
|
||||
RotateSignedPreKeyListener: WhatIsThis;
|
||||
|
||||
AlreadyGroupMemberToast: typeof window.Whisper.ToastView;
|
||||
|
@ -630,6 +635,7 @@ export type WhisperType = {
|
|||
CaptchaSolvedToast: typeof window.Whisper.ToastView;
|
||||
CaptchaFailedToast: typeof window.Whisper.ToastView;
|
||||
DangerousFileTypeToast: typeof window.Whisper.ToastView;
|
||||
DecryptionErrorToast: typeof window.Whisper.ToastView;
|
||||
ExpiredToast: typeof window.Whisper.ToastView;
|
||||
FileSavedToast: typeof window.Whisper.ToastView;
|
||||
FileSizeToast: any;
|
||||
|
|
Loading…
Reference in a new issue