Merge contacts when we discover split or duplicated contacts

This commit is contained in:
Scott Nonnenberg 2020-07-10 11:28:49 -07:00
parent 68e432188b
commit 901179440f
32 changed files with 1199 additions and 824 deletions

View file

@ -348,7 +348,6 @@
<script type='text/javascript' src='js/expiring_tap_to_view_messages.js'></script> <script type='text/javascript' src='js/expiring_tap_to_view_messages.js'></script>
<script type='text/javascript' src='js/chromium.js'></script> <script type='text/javascript' src='js/chromium.js'></script>
<script type='text/javascript' src='js/conversation_controller.js'></script>
<script type='text/javascript' src='js/message_controller.js'></script> <script type='text/javascript' src='js/message_controller.js'></script>
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script> <script type='text/javascript' src='js/views/react_wrapper_view.js'></script>

View file

@ -25,14 +25,24 @@
wait: 500, wait: 500,
maxSize: 500, maxSize: 500,
processBatch: async items => { processBatch: async items => {
const bySource = _.groupBy(items, item => item.source || item.sourceUuid); const byConversationId = _.groupBy(items, item =>
const sources = Object.keys(bySource); ConversationController.ensureContactIds({
e164: item.source,
uuid: item.sourceUuid,
})
);
const ids = Object.keys(byConversationId);
for (let i = 0, max = sources.length; i < max; i += 1) { for (let i = 0, max = ids.length; i < max; i += 1) {
const source = sources[i]; const conversationId = ids[i];
const timestamps = bySource[source].map(item => item.timestamp); const timestamps = byConversationId[conversationId].map(
item => item.timestamp
);
const c = ConversationController.get(conversationId);
const uuid = c.get('uuid');
const e164 = c.get('e164');
const c = ConversationController.get(source);
c.queueJob(async () => { c.queueJob(async () => {
try { try {
const { wrap, sendOptions } = ConversationController.prepareForSend( const { wrap, sendOptions } = ConversationController.prepareForSend(
@ -41,15 +51,15 @@
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await wrap( await wrap(
textsecure.messaging.sendDeliveryReceipt( textsecure.messaging.sendDeliveryReceipt(
c.get('e164'), e164,
c.get('uuid'), uuid,
timestamps, timestamps,
sendOptions sendOptions
) )
); );
} catch (error) { } catch (error) {
window.log.error( window.log.error(
`Failed to send delivery receipt to ${source} for timestamps ${timestamps}:`, `Failed to send delivery receipt to ${e164}/${uuid} for timestamps ${timestamps}:`,
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
} }
@ -577,6 +587,7 @@
} }
}); });
window.Signal.conversationControllerStart();
try { try {
await Promise.all([ await Promise.all([
ConversationController.load(), ConversationController.load(),
@ -584,6 +595,7 @@
Signal.Emojis.load(), Signal.Emojis.load(),
textsecure.storage.protocol.hydrateCaches(), textsecure.storage.protocol.hydrateCaches(),
]); ]);
await ConversationController.checkForConflicts();
} catch (error) { } catch (error) {
window.log.error( window.log.error(
'background.js: ConversationController failed to load:', 'background.js: ConversationController failed to load:',
@ -1759,6 +1771,7 @@
const deviceId = textsecure.storage.user.getDeviceId(); const deviceId = textsecure.storage.user.getDeviceId();
// If we didn't capture a UUID on registration, go get it from the server
if (!textsecure.storage.user.getUuid()) { if (!textsecure.storage.user.getUuid()) {
const server = WebAPI.connect({ const server = WebAPI.connect({
username: OLD_USERNAME, username: OLD_USERNAME,
@ -1954,6 +1967,7 @@
const senderId = ConversationController.ensureContactIds({ const senderId = ConversationController.ensureContactIds({
e164: sender, e164: sender,
uuid: senderUuid, uuid: senderUuid,
highTrust: true,
}); });
const conversation = ConversationController.get(groupId || senderId); const conversation = ConversationController.get(groupId || senderId);
const ourId = ConversationController.getOurConversationId(); const ourId = ConversationController.getOurConversationId();
@ -2047,6 +2061,7 @@
const detailsId = ConversationController.ensureContactIds({ const detailsId = ConversationController.ensureContactIds({
e164: details.number, e164: details.number,
uuid: details.uuid, uuid: details.uuid,
highTrust: true,
}); });
const conversation = ConversationController.get(detailsId); const conversation = ConversationController.get(detailsId);
let activeAt = conversation.get('active_at'); let activeAt = conversation.get('active_at');
@ -2236,12 +2251,10 @@
return; return;
} }
const sourceE164 = textsecure.storage.user.getNumber();
const sourceUuid = textsecure.storage.user.getUuid();
const receivedAt = Date.now(); const receivedAt = Date.now();
await conversation.updateExpirationTimer( await conversation.updateExpirationTimer(
expireTimer, expireTimer,
sourceE164 || sourceUuid, ConversationController.getOurConversationId(),
receivedAt, receivedAt,
{ {
fromSync: true, fromSync: true,
@ -2256,10 +2269,16 @@
}); });
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`: // Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
const getDescriptorForSent = ({ message, destination }) => const getDescriptorForSent = ({ message, destination, destinationUuid }) =>
message.group message.group
? getGroupDescriptor(message.group) ? getGroupDescriptor(message.group)
: { type: Message.PRIVATE, id: destination }; : {
type: Message.PRIVATE,
id: ConversationController.ensureContactIds({
e164: destination,
uuid: destinationUuid,
}),
};
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`: // Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
const getDescriptorForReceived = ({ message, source, sourceUuid }) => const getDescriptorForReceived = ({ message, source, sourceUuid }) =>
@ -2270,6 +2289,7 @@
id: ConversationController.ensureContactIds({ id: ConversationController.ensureContactIds({
e164: source, e164: source,
uuid: sourceUuid, uuid: sourceUuid,
highTrust: true,
}), }),
}; };
@ -2280,13 +2300,12 @@
messageDescriptor, messageDescriptor,
}) { }) {
const profileKey = data.message.profileKey.toString('base64'); const profileKey = data.message.profileKey.toString('base64');
const sender = await ConversationController.getOrCreateAndWait( const sender = await ConversationController.get(messageDescriptor.id);
messageDescriptor.id,
'private'
);
// Will do the save for us if (sender) {
await sender.setProfileKey(profileKey); // Will do the save for us
await sender.setProfileKey(profileKey);
}
return confirm(); return confirm();
} }
@ -2357,9 +2376,12 @@
} }
async function onProfileKeyUpdate({ data, confirm }) { async function onProfileKeyUpdate({ data, confirm }) {
const conversation = ConversationController.get( const conversationId = ConversationController.ensureContactIds({
data.source || data.sourceUuid e164: data.source,
); uuid: data.sourceUuid,
highTrust: true,
});
const conversation = ConversationController.get(conversationId);
if (!conversation) { if (!conversation) {
window.log.error( window.log.error(
@ -2397,11 +2419,8 @@
messageDescriptor, messageDescriptor,
}) { }) {
// First set profileSharing = true for the conversation we sent to // First set profileSharing = true for the conversation we sent to
const { id, type } = messageDescriptor; const { id } = messageDescriptor;
const conversation = await ConversationController.getOrCreateAndWait( const conversation = await ConversationController.get(id);
id,
type
);
conversation.enableProfileSharing(); conversation.enableProfileSharing();
window.Signal.Data.updateConversation(conversation.attributes); window.Signal.Data.updateConversation(conversation.attributes);
@ -2417,7 +2436,7 @@
return confirm(); return confirm();
} }
function createSentMessage(data) { function createSentMessage(data, descriptor) {
const now = Date.now(); const now = Date.now();
let sentTo = []; let sentTo = [];
@ -2430,6 +2449,11 @@
data.unidentifiedDeliveries = unidentified.map(item => item.destination); data.unidentifiedDeliveries = unidentified.map(item => item.destination);
} }
const isGroup = descriptor.type === Message.GROUP;
const conversationId = isGroup
? ConversationController.ensureGroup(descriptor.id)
: descriptor.id;
return new Whisper.Message({ return new Whisper.Message({
source: textsecure.storage.user.getNumber(), source: textsecure.storage.user.getNumber(),
sourceUuid: textsecure.storage.user.getUuid(), sourceUuid: textsecure.storage.user.getUuid(),
@ -2438,7 +2462,7 @@
serverTimestamp: data.serverTimestamp, serverTimestamp: data.serverTimestamp,
sent_to: sentTo, sent_to: sentTo,
received_at: now, received_at: now,
conversationId: data.destination, conversationId,
type: 'outgoing', type: 'outgoing',
sent: true, sent: true,
unidentifiedDeliveries: data.unidentifiedDeliveries || [], unidentifiedDeliveries: data.unidentifiedDeliveries || [],
@ -2468,7 +2492,7 @@
}); });
} }
const message = createSentMessage(data); const message = createSentMessage(data, messageDescriptor);
if (data.message.reaction) { if (data.message.reaction) {
const { reaction } = data.message; const { reaction } = data.message;
@ -2502,12 +2526,7 @@
return Promise.resolve(); return Promise.resolve();
} }
ConversationController.getOrCreate(
messageDescriptor.id,
messageDescriptor.type
);
// Don't wait for handleDataMessage, as it has its own per-conversation queueing // Don't wait for handleDataMessage, as it has its own per-conversation queueing
message.handleDataMessage(data.message, event.confirm, { message.handleDataMessage(data.message, event.confirm, {
data, data,
}); });
@ -2520,12 +2539,10 @@
const fromContactId = ConversationController.ensureContactIds({ const fromContactId = ConversationController.ensureContactIds({
e164: data.source, e164: data.source,
uuid: data.sourceUuid, uuid: data.sourceUuid,
highTrust: true,
}); });
// Determine if this message is in a group
const isGroup = descriptor.type === Message.GROUP; const isGroup = descriptor.type === Message.GROUP;
// Determine the conversationId this message belongs to
const conversationId = isGroup const conversationId = isGroup
? ConversationController.ensureGroup(descriptor.id, { ? ConversationController.ensureGroup(descriptor.id, {
addedBy: fromContactId, addedBy: fromContactId,
@ -2651,10 +2668,7 @@
}); });
const conversationId = message.get('conversationId'); const conversationId = message.get('conversationId');
const conversation = ConversationController.getOrCreate( const conversation = ConversationController.get(conversationId);
conversationId,
'private'
);
// This matches the queueing behavior used in Message.handleDataMessage // This matches the queueing behavior used in Message.handleDataMessage
conversation.queueJob(async () => { conversation.queueJob(async () => {
@ -2791,9 +2805,13 @@
function onReadReceipt(ev) { function onReadReceipt(ev) {
const readAt = ev.timestamp; const readAt = ev.timestamp;
const { timestamp } = ev.read; const { timestamp, source, sourceUuid } = ev.read;
const reader = ConversationController.getConversationId(ev.read.reader); const reader = ConversationController.ensureContactIds({
window.log.info('read receipt', reader, timestamp); e164: source,
uuid: sourceUuid,
highTrust: true,
});
window.log.info('read receipt', timestamp, source, sourceUuid, reader);
ev.confirm(); ev.confirm();
@ -2879,7 +2897,11 @@
ev.viaContactSync ? 'via contact sync' : '' ev.viaContactSync ? 'via contact sync' : ''
); );
const verifiedId = ConversationController.ensureContactIds({ e164, uuid }); const verifiedId = ConversationController.ensureContactIds({
e164,
uuid,
highTrust: true,
});
const contact = await ConversationController.get(verifiedId, 'private'); const contact = await ConversationController.get(verifiedId, 'private');
const options = { const options = {
viaSyncMessage: true, viaSyncMessage: true,
@ -2899,20 +2921,23 @@
function onDeliveryReceipt(ev) { function onDeliveryReceipt(ev) {
const { deliveryReceipt } = ev; const { deliveryReceipt } = ev;
const { sourceUuid, source } = deliveryReceipt; const { sourceUuid, source } = deliveryReceipt;
const identifier = source || sourceUuid;
window.log.info( window.log.info(
'delivery receipt from', 'delivery receipt from',
`${identifier}.${deliveryReceipt.sourceDevice}`, `${source} ${sourceUuid} ${deliveryReceipt.sourceDevice}`,
deliveryReceipt.timestamp deliveryReceipt.timestamp
); );
ev.confirm(); ev.confirm();
const deliveredTo = ConversationController.getConversationId(identifier); const deliveredTo = ConversationController.ensureContactIds({
e164: source,
uuid: sourceUuid,
highTrust: true,
});
if (!deliveredTo) { if (!deliveredTo) {
window.log.info('no conversation for identifier', identifier); window.log.info('no conversation for', source, sourceUuid);
return; return;
} }

View file

@ -1,353 +0,0 @@
/* global _, Whisper, Backbone, storage, textsecure */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
const conversations = new Whisper.ConversationCollection();
const inboxCollection = new (Backbone.Collection.extend({
initialize() {
this.listenTo(conversations, 'add change:active_at', this.addActive);
this.listenTo(conversations, 'reset', () => this.reset([]));
this.on(
'add remove change:unreadCount',
_.debounce(this.updateUnreadCount.bind(this), 1000)
);
},
addActive(model) {
if (model.get('active_at')) {
this.add(model);
} else {
this.remove(model);
}
},
updateUnreadCount() {
const newUnreadCount = _.reduce(
this.map(m => m.get('unreadCount')),
(item, memo) => item + memo,
0
);
storage.put('unreadCount', newUnreadCount);
if (newUnreadCount > 0) {
window.setBadgeCount(newUnreadCount);
window.document.title = `${window.getTitle()} (${newUnreadCount})`;
} else {
window.setBadgeCount(0);
window.document.title = window.getTitle();
}
window.updateTrayIcon(newUnreadCount);
},
}))();
window.getInboxCollection = () => inboxCollection;
window.getConversations = () => conversations;
window.ConversationController = {
get(id) {
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
return conversations.get(id);
},
// Needed for some model setup which happens during the initial fetch() call below
getUnsafe(id) {
return conversations.get(id);
},
dangerouslyCreateAndAdd(attributes) {
return conversations.add(attributes);
},
getOrCreate(identifier, type, additionalInitialProps = {}) {
if (typeof identifier !== 'string') {
throw new TypeError("'id' must be a string");
}
if (type !== 'private' && type !== 'group') {
throw new TypeError(
`'type' must be 'private' or 'group'; got: '${type}'`
);
}
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
let conversation = conversations.get(identifier);
if (conversation) {
return conversation;
}
const id = window.getGuid();
if (type === 'group') {
conversation = conversations.add({
id,
uuid: null,
e164: null,
groupId: identifier,
type,
version: 2,
...additionalInitialProps,
});
} else if (window.isValidGuid(identifier)) {
conversation = conversations.add({
id,
uuid: identifier,
e164: null,
groupId: null,
type,
version: 2,
...additionalInitialProps,
});
} else {
conversation = conversations.add({
id,
uuid: null,
e164: identifier,
groupId: null,
type,
version: 2,
...additionalInitialProps,
});
}
const create = async () => {
if (!conversation.isValid()) {
const validationError = conversation.validationError || {};
window.log.error(
'Contact is not valid. Not saving, but adding to collection:',
conversation.idForLogging(),
validationError.stack
);
return conversation;
}
try {
await window.Signal.Data.saveConversation(conversation.attributes, {
Conversation: Whisper.Conversation,
});
} catch (error) {
window.log.error(
'Conversation save failed! ',
identifier,
type,
'Error:',
error && error.stack ? error.stack : error
);
throw error;
}
return conversation;
};
conversation.initialPromise = create();
return conversation;
},
getOrCreateAndWait(id, type, additionalInitialProps = {}) {
return this._initialPromise.then(() => {
const conversation = this.getOrCreate(id, type, additionalInitialProps);
if (conversation) {
return conversation.initialPromise.then(() => conversation);
}
return Promise.reject(
new Error('getOrCreateAndWait: did not get conversation')
);
});
},
getConversationId(address) {
if (!address) {
return null;
}
const [id] = textsecure.utils.unencodeNumber(address);
const conv = this.get(id);
if (conv) {
return conv.get('id');
}
return null;
},
getOurConversationId() {
const e164 = textsecure.storage.user.getNumber();
const uuid = textsecure.storage.user.getUuid();
return this.ensureContactIds({ e164, uuid });
},
/**
* Given a UUID and/or an E164, resolves to a string representing the local
* database of the given contact. If a conversation is found it is updated
* to have the given UUID and E164. If a conversation is not found, this
* function creates a conversation with the given UUID and E164. If the
* conversation * is found in the local database it is updated.
*
* This function also additionally checks for mismatched e164/uuid pairs out
* of abundance of caution.
*/
ensureContactIds({ e164, uuid }) {
// Check for at least one parameter being provided. This is necessary
// because this path can be called on startup to resolve our own ID before
// our phone number or UUID are known. The existing behavior in these
// cases can handle a returned `undefined` id, so we do that.
if (!e164 && !uuid) {
return undefined;
}
const lowerUuid = uuid ? uuid.toLowerCase() : undefined;
const convoE164 = this.get(e164);
const convoUuid = this.get(lowerUuid);
// Check for mismatched UUID and E164
if (
convoE164 &&
convoUuid &&
convoE164.get('id') !== convoUuid.get('id')
) {
window.log.warn('Received a message with a mismatched UUID and E164.');
}
const convo = convoUuid || convoE164;
const idOrIdentifier = convo ? convo.get('id') : e164 || lowerUuid;
const finalConversation = this.getOrCreate(idOrIdentifier, 'private');
finalConversation.updateE164(e164);
finalConversation.updateUuid(lowerUuid);
return finalConversation.get('id');
},
/**
* Given a groupId and optional additional initialization properties,
* ensures the existence of a group conversation and returns a string
* representing the local database ID of the group conversation.
*/
ensureGroup(groupId, additionalInitProps = {}) {
return this.getOrCreate(groupId, 'group', additionalInitProps).get('id');
},
/**
* Given certain metadata about a message (an identifier of who wrote the
* message and the sent_at timestamp of the message) returns the
* conversation the message belongs to OR null if a conversation isn't
* found.
* @param {string} targetFrom The E164, UUID, or Conversation ID of the message author
* @param {number} targetTimestamp The sent_at timestamp of the target message
*/
async getConversationForTargetMessage(targetFrom, targetTimestamp) {
const targetFromId = this.getConversationId(targetFrom);
const messages = await window.Signal.Data.getMessagesBySentAt(
targetTimestamp,
{
MessageCollection: Whisper.MessageCollection,
}
);
const targetMessage = messages.find(m => {
const contact = m.getContact();
if (!contact) {
return false;
}
const mcid = contact.get('id');
return mcid === targetFromId;
});
if (targetMessage) {
return targetMessage.getConversation();
}
return null;
},
prepareForSend(id, options) {
// id is any valid conversation identifier
const conversation = this.get(id);
const sendOptions = conversation
? conversation.getSendOptions(options)
: null;
const wrap = conversation
? conversation.wrapSend.bind(conversation)
: promise => promise;
return { wrap, sendOptions };
},
async getAllGroupsInvolvingId(conversationId) {
const groups = await window.Signal.Data.getAllGroupsInvolvingId(
conversationId,
{
ConversationCollection: Whisper.ConversationCollection,
}
);
return groups.map(group => conversations.add(group));
},
loadPromise() {
return this._initialPromise;
},
reset() {
this._initialPromise = Promise.resolve();
this._initialFetchComplete = false;
conversations.reset([]);
},
async load() {
window.log.info('ConversationController: starting initial fetch');
if (conversations.length) {
throw new Error('ConversationController: Already loaded!');
}
const load = async () => {
try {
const collection = await window.Signal.Data.getAllConversations({
ConversationCollection: Whisper.ConversationCollection,
});
conversations.add(collection.models);
this._initialFetchComplete = true;
await Promise.all(
conversations.map(async conversation => {
if (!conversation.get('lastMessage')) {
await conversation.updateLastMessage();
}
// In case a too-large draft was saved to the database
const draft = conversation.get('draft');
if (draft && draft.length > MAX_MESSAGE_BODY_LENGTH) {
this.model.set({
draft: draft.slice(0, MAX_MESSAGE_BODY_LENGTH),
});
window.Signal.Data.updateConversation(conversation.attributes);
}
})
);
window.log.info('ConversationController: done with initial fetch');
} catch (error) {
window.log.error(
'ConversationController: initial fetch failed',
error && error.stack ? error.stack : error
);
throw error;
}
};
this._initialPromise = load();
return this._initialPromise;
},
};
})();

View file

@ -30,11 +30,10 @@
this.remove(receipts); this.remove(receipts);
return receipts; return receipts;
}, },
async getTargetMessage(source, messages) { async getTargetMessage(sourceId, messages) {
if (messages.length === 0) { if (messages.length === 0) {
return null; return null;
} }
const sourceId = ConversationController.getConversationId(source);
const message = messages.find( const message = messages.find(
item => !item.isIncoming() && sourceId === item.get('conversationId') item => !item.isIncoming() && sourceId === item.get('conversationId')
); );

View file

@ -15,7 +15,7 @@
forConversation(conversation) { forConversation(conversation) {
if (conversation.get('e164')) { if (conversation.get('e164')) {
const syncByE164 = this.findWhere({ const syncByE164 = this.findWhere({
e164: conversation.get('e164'), threadE164: conversation.get('e164'),
}); });
if (syncByE164) { if (syncByE164) {
window.log.info( window.log.info(
@ -30,7 +30,7 @@
if (conversation.get('uuid')) { if (conversation.get('uuid')) {
const syncByUuid = this.findWhere({ const syncByUuid = this.findWhere({
uuid: conversation.get('uuid'), threadUuid: conversation.get('uuid'),
}); });
if (syncByUuid) { if (syncByUuid) {
window.log.info( window.log.info(
@ -45,7 +45,7 @@
if (conversation.get('groupId')) { if (conversation.get('groupId')) {
const syncByGroupId = this.findWhere({ const syncByGroupId = this.findWhere({
uuid: conversation.get('groupId'), groupId: conversation.get('groupId'),
}); });
if (syncByGroupId) { if (syncByGroupId) {
window.log.info( window.log.info(
@ -65,12 +65,19 @@
const threadE164 = sync.get('threadE164'); const threadE164 = sync.get('threadE164');
const threadUuid = sync.get('threadUuid'); const threadUuid = sync.get('threadUuid');
const groupId = sync.get('groupId'); const groupId = sync.get('groupId');
const identifier = threadE164 || threadUuid || groupId;
const conversation = ConversationController.get(identifier); const conversation = groupId
? ConversationController.get(groupId)
: ConversationController.get(
ConversationController.ensureContactIds({
e164: threadE164,
uuid: threadUuid,
})
);
if (!conversation) { if (!conversation) {
window.log( window.log(
`Received message request response for unknown conversation: ${identifier}` `Received message request response for unknown conversation: ${groupId} ${threadUuid} ${threadE164}`
); );
return; return;
} }

View file

@ -85,6 +85,12 @@
return `group(${groupId})`; return `group(${groupId})`;
}, },
// This is one of the few times that we want to collapse our uuid/e164 pair down into
// just one bit of data. If we have a UUID, we'll send using it.
getSendTarget() {
return this.get('uuid') || this.get('e164');
},
handleMessageError(message, errors) { handleMessageError(message, errors) {
this.trigger('messageError', message, errors); this.trigger('messageError', message, errors);
}, },
@ -318,9 +324,8 @@
} }
const groupId = !this.isPrivate() ? this.get('groupId') : null; const groupId = !this.isPrivate() ? this.get('groupId') : null;
const maybeRecipientId = this.get('uuid') || this.get('e164');
const recipientId = this.isPrivate() ? maybeRecipientId : null;
const groupNumbers = this.getRecipients(); const groupNumbers = this.getRecipients();
const recipientId = this.isPrivate() ? this.getSendTarget() : null;
const sendOptions = this.getSendOptions(); const sendOptions = this.getSendOptions();
@ -395,10 +400,10 @@
async onNewMessage(message) { async onNewMessage(message) {
// Clear typing indicator for a given contact if we receive a message from them // Clear typing indicator for a given contact if we receive a message from them
const identifier = message.get const deviceId = message.get
? `${message.get('source')}.${message.get('sourceDevice')}` ? `${message.get('conversationId')}.${message.get('sourceDevice')}`
: `${message.source}.${message.sourceDevice}`; : `${message.conversationId}.${message.sourceDevice}`;
this.clearContactTypingTimer(identifier); this.clearContactTypingTimer(deviceId);
this.debouncedUpdateLastMessage(); this.debouncedUpdateLastMessage();
}, },
@ -582,7 +587,13 @@
m => !m.hasErrors() && m.isIncoming() m => !m.hasErrors() && m.isIncoming()
); );
const receiptSpecs = readMessages.map(m => ({ const receiptSpecs = readMessages.map(m => ({
sender: m.get('source') || m.get('sourceUuid'), senderE164: m.get('source'),
senderUuid: m.get('sourceUuid'),
senderId: ConversationController.get({
e164: m.get('source'),
uuid: m.get('sourceUuid'),
lowTrust: true,
}),
timestamp: m.get('sent_at'), timestamp: m.get('sent_at'),
hasErrors: m.hasErrors(), hasErrors: m.hasErrors(),
})); }));
@ -1167,14 +1178,12 @@
getRecipients() { getRecipients() {
if (this.isPrivate()) { if (this.isPrivate()) {
return [this.get('uuid') || this.get('e164')]; return [this.getSendTarget()];
} }
const me = ConversationController.getConversationId( const me = ConversationController.getOurConversationId();
textsecure.storage.user.getUuid() || textsecure.storage.user.getNumber()
);
return _.without(this.get('members'), me).map(memberId => { return _.without(this.get('members'), me).map(memberId => {
const c = ConversationController.get(memberId); const c = ConversationController.get(memberId);
return c.get('uuid') || c.get('e164'); return c.getSendTarget();
}); });
}, },
@ -1331,11 +1340,7 @@
const reactionModel = Whisper.Reactions.add({ const reactionModel = Whisper.Reactions.add({
...outgoingReaction, ...outgoingReaction,
fromId: fromId: ConversationController.getOurConversationId(),
this.ourNumber ||
this.ourUuid ||
textsecure.storage.user.getNumber() ||
textsecure.storage.user.getUuid(),
timestamp, timestamp,
fromSync: true, fromSync: true,
}); });
@ -1446,9 +1451,7 @@
async sendProfileKeyUpdate() { async sendProfileKeyUpdate() {
const id = this.get('id'); const id = this.get('id');
const recipients = this.isPrivate() const recipients = this.getRecipients();
? [this.get('uuid') || this.get('e164')]
: this.getRecipients();
if (!this.get('profileSharing')) { if (!this.get('profileSharing')) {
window.log.error( window.log.error(
'Attempted to send profileKeyUpdate to conversation without profileSharing enabled', 'Attempted to send profileKeyUpdate to conversation without profileSharing enabled',
@ -1477,7 +1480,7 @@
const { clearUnreadMetrics } = window.reduxActions.conversations; const { clearUnreadMetrics } = window.reduxActions.conversations;
clearUnreadMetrics(this.id); clearUnreadMetrics(this.id);
const destination = this.get('uuid') || this.get('e164'); const destination = this.getSendTarget();
const expireTimer = this.get('expireTimer'); const expireTimer = this.get('expireTimer');
const recipients = this.getRecipients(); const recipients = this.getRecipients();
@ -1549,7 +1552,7 @@
).map(contact => { ).map(contact => {
const error = new Error('Network is not available'); const error = new Error('Network is not available');
error.name = 'SendMessageNetworkError'; error.name = 'SendMessageNetworkError';
error.identifier = contact.get('uuid') || contact.get('e164'); error.identifier = contact.get('id');
return error; return error;
}); });
await message.saveErrors(errors); await message.saveErrors(errors);
@ -1658,8 +1661,8 @@
async handleMessageSendResult(failoverIdentifiers, unidentifiedDeliveries) { async handleMessageSendResult(failoverIdentifiers, unidentifiedDeliveries) {
await Promise.all( await Promise.all(
(failoverIdentifiers || []).map(async number => { (failoverIdentifiers || []).map(async identifier => {
const conversation = ConversationController.get(number); const conversation = ConversationController.get(identifier);
if ( if (
conversation && conversation &&
@ -1677,8 +1680,8 @@
); );
await Promise.all( await Promise.all(
(unidentifiedDeliveries || []).map(async number => { (unidentifiedDeliveries || []).map(async identifier => {
const conversation = ConversationController.get(number); const conversation = ConversationController.get(identifier);
if ( if (
conversation && conversation &&
@ -1722,17 +1725,10 @@
getSendMetadata(options = {}) { getSendMetadata(options = {}) {
const { syncMessage, disableMeCheck } = options; const { syncMessage, disableMeCheck } = options;
if (!this.ourNumber && !this.ourUuid) {
return null;
}
// START: this code has an Expiration date of ~2018/11/21 // START: this code has an Expiration date of ~2018/11/21
// We don't want to enable unidentified delivery for send unless it is // We don't want to enable unidentified delivery for send unless it is
// also enabled for our own account. // also enabled for our own account.
const myId = ConversationController.ensureContactIds({ const myId = ConversationController.getOurConversationId();
e164: this.ourNumber,
uuid: this.ourUuid,
});
const me = ConversationController.get(myId); const me = ConversationController.get(myId);
if ( if (
!disableMeCheck && !disableMeCheck &&
@ -1903,10 +1899,7 @@
source, source,
}); });
source = source = source || ConversationController.getOurConversationId();
source ||
textsecure.storage.user.getNumber() ||
textsecure.storage.user.getUuid();
// When we add a disappearing messages notification to the conversation, we want it // When we add a disappearing messages notification to the conversation, we want it
// to be above the message that initiated that change, hence the subtraction. // to be above the message that initiated that change, hence the subtraction.
@ -1933,7 +1926,7 @@
}); });
if (this.isPrivate()) { if (this.isPrivate()) {
model.set({ destination: this.get('uuid') || this.get('e164') }); model.set({ destination: this.getSendTarget() });
} }
if (model.isOutgoing()) { if (model.isOutgoing()) {
model.set({ recipients: this.getRecipients() }); model.set({ recipients: this.getRecipients() });
@ -1963,7 +1956,7 @@
const flags = const flags =
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
const dataMessage = await textsecure.messaging.getMessageProto( const dataMessage = await textsecure.messaging.getMessageProto(
this.get('uuid') || this.get('e164'), this.getSendTarget(),
null, null,
[], [],
null, null,
@ -1980,7 +1973,7 @@
if (this.get('type') === 'private') { if (this.get('type') === 'private') {
promise = textsecure.messaging.sendExpirationTimerUpdateToIdentifier( promise = textsecure.messaging.sendExpirationTimerUpdateToIdentifier(
this.get('uuid') || this.get('e164'), this.getSendTarget(),
expireTimer, expireTimer,
message.get('sent_at'), message.get('sent_at'),
profileKey, profileKey,
@ -2180,7 +2173,12 @@
await m.markRead(options.readAt); await m.markRead(options.readAt);
return { return {
sender: m.get('source') || m.get('sourceUuid'), senderE164: m.get('source'),
senderUuid: m.get('sourceUuid'),
senderId: ConversationController.ensureContactIds({
e164: m.get('source'),
uuid: m.get('sourceUuid'),
}),
timestamp: m.get('sent_at'), timestamp: m.get('sent_at'),
hasErrors: m.hasErrors(), hasErrors: m.hasErrors(),
}; };
@ -2188,7 +2186,7 @@
); );
// Some messages we're marking read are local notifications with no sender // Some messages we're marking read are local notifications with no sender
read = _.filter(read, m => Boolean(m.sender)); read = _.filter(read, m => Boolean(m.senderId));
unreadMessages = unreadMessages.filter(m => Boolean(m.isIncoming())); unreadMessages = unreadMessages.filter(m => Boolean(m.isIncoming()));
const unreadCount = unreadMessages.length - read.length; const unreadCount = unreadMessages.length - read.length;
@ -2206,8 +2204,10 @@
window.log.info(`Sending ${read.length} read syncs`); window.log.info(`Sending ${read.length} read syncs`);
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes // Because syncReadMessages sends to our other devices, and sendReadReceipts goes
// to a contact, we need accessKeys for both. // to a contact, we need accessKeys for both.
const { sendOptions } = ConversationController.prepareForSend( const {
this.ourUuid || this.ourNumber, sendOptions,
} = ConversationController.prepareForSend(
ConversationController.getOurConversationId(),
{ syncMessage: true } { syncMessage: true }
); );
await this.wrapSend( await this.wrapSend(
@ -2222,12 +2222,12 @@
if (storage.get('read-receipt-setting') && this.getAccepted()) { if (storage.get('read-receipt-setting') && this.getAccepted()) {
window.log.info(`Sending ${items.length} read receipts`); window.log.info(`Sending ${items.length} read receipts`);
const convoSendOptions = this.getSendOptions(); const convoSendOptions = this.getSendOptions();
const receiptsBySender = _.groupBy(items, 'sender'); const receiptsBySender = _.groupBy(items, 'senderId');
await Promise.all( await Promise.all(
_.map(receiptsBySender, async (receipts, identifier) => { _.map(receiptsBySender, async (receipts, senderId) => {
const timestamps = _.map(receipts, 'timestamp'); const timestamps = _.map(receipts, 'timestamp');
const c = ConversationController.get(identifier); const c = ConversationController.get(senderId);
await this.wrapSend( await this.wrapSend(
textsecure.messaging.sendReadReceipts( textsecure.messaging.sendReadReceipts(
c.get('e164'), c.get('e164'),
@ -2249,28 +2249,33 @@
getProfiles() { getProfiles() {
// request all conversation members' keys // request all conversation members' keys
let ids = []; let conversations = [];
if (this.isPrivate()) { if (this.isPrivate()) {
ids = [this.get('uuid') || this.get('e164')]; conversations = [this];
} else { } else {
ids = this.get('members') conversations = this.get('members')
.map(id => { .map(id => ConversationController.get(id))
const c = ConversationController.get(id);
return c ? c.get('uuid') || c.get('e164') : null;
})
.filter(Boolean); .filter(Boolean);
} }
return Promise.all(_.map(ids, this.getProfile)); return Promise.all(
_.map(conversations, conversation => {
this.getProfile(conversation.get('uuid'), conversation.get('e164'));
})
);
}, },
async getProfile(id) { async getProfile(providedUuid, providedE164) {
if (!textsecure.messaging) { if (!textsecure.messaging) {
throw new Error( throw new Error(
'Conversation.getProfile: textsecure.messaging not available' 'Conversation.getProfile: textsecure.messaging not available'
); );
} }
const c = ConversationController.getOrCreate(id, 'private'); const id = ConversationController.ensureContactIds({
uuid: providedUuid,
e164: providedE164,
});
const c = ConversationController.get(id);
const { const {
generateProfileKeyCredentialRequest, generateProfileKeyCredentialRequest,
getClientZkProfileOperations, getClientZkProfileOperations,
@ -2294,6 +2299,7 @@
const profileKey = c.get('profileKey'); const profileKey = c.get('profileKey');
const uuid = c.get('uuid'); const uuid = c.get('uuid');
const identifier = c.getSendTarget();
const profileKeyVersionHex = c.get('profileKeyVersion'); const profileKeyVersionHex = c.get('profileKeyVersion');
const existingProfileKeyCredential = c.get('profileKeyCredential'); const existingProfileKeyCredential = c.get('profileKeyCredential');
@ -2317,11 +2323,11 @@
const sendMetadata = c.getSendMetadata({ disableMeCheck: true }) || {}; const sendMetadata = c.getSendMetadata({ disableMeCheck: true }) || {};
const getInfo = const getInfo =
sendMetadata[c.get('e164')] || sendMetadata[c.get('uuid')] || {}; sendMetadata[c.get('uuid')] || sendMetadata[c.get('e164')] || {};
if (getInfo.accessKey) { if (getInfo.accessKey) {
try { try {
profile = await textsecure.messaging.getProfile(id, { profile = await textsecure.messaging.getProfile(identifier, {
accessKey: getInfo.accessKey, accessKey: getInfo.accessKey,
profileKeyVersion: profileKeyVersionHex, profileKeyVersion: profileKeyVersionHex,
profileKeyCredentialRequest: profileKeyCredentialRequestHex, profileKeyCredentialRequest: profileKeyCredentialRequestHex,
@ -2332,7 +2338,7 @@
`Setting sealedSender to DISABLED for conversation ${c.idForLogging()}` `Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
); );
c.set({ sealedSender: SEALED_SENDER.DISABLED }); c.set({ sealedSender: SEALED_SENDER.DISABLED });
profile = await textsecure.messaging.getProfile(id, { profile = await textsecure.messaging.getProfile(identifier, {
profileKeyVersion: profileKeyVersionHex, profileKeyVersion: profileKeyVersionHex,
profileKeyCredentialRequest: profileKeyCredentialRequestHex, profileKeyCredentialRequest: profileKeyCredentialRequestHex,
}); });
@ -2341,7 +2347,7 @@
} }
} }
} else { } else {
profile = await textsecure.messaging.getProfile(id, { profile = await textsecure.messaging.getProfile(identifier, {
profileKeyVersion: profileKeyVersionHex, profileKeyVersion: profileKeyVersionHex,
profileKeyCredentialRequest: profileKeyCredentialRequestHex, profileKeyCredentialRequest: profileKeyCredentialRequestHex,
}); });
@ -2349,14 +2355,14 @@
const identityKey = base64ToArrayBuffer(profile.identityKey); const identityKey = base64ToArrayBuffer(profile.identityKey);
const changed = await textsecure.storage.protocol.saveIdentity( const changed = await textsecure.storage.protocol.saveIdentity(
`${id}.1`, `${identifier}.1`,
identityKey, identityKey,
false false
); );
if (changed) { if (changed) {
// save identity will close all sessions except for .1, so we // save identity will close all sessions except for .1, so we
// must close that one manually. // must close that one manually.
const address = new libsignal.SignalProtocolAddress(id, 1); const address = new libsignal.SignalProtocolAddress(identifier, 1);
window.log.info('closing session for', address.toString()); window.log.info('closing session for', address.toString());
const sessionCipher = new libsignal.SessionCipher( const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol, textsecure.storage.protocol,
@ -2421,7 +2427,7 @@
if (error.code !== 403 && error.code !== 404) { if (error.code !== 403 && error.code !== 404) {
window.log.warn( window.log.warn(
'getProfile failure:', 'getProfile failure:',
id, c.idForLogging(),
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
} else { } else {
@ -2435,7 +2441,7 @@
} catch (error) { } catch (error) {
window.log.warn( window.log.warn(
'getProfile decryption failure:', 'getProfile decryption failure:',
id, c.idForLogging(),
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
await c.dropProfileKey(); await c.dropProfileKey();
@ -2780,10 +2786,9 @@
const conversationId = this.id; const conversationId = this.id;
const sender = await ConversationController.getOrCreateAndWait( const sender = reaction
reaction ? reaction.get('fromId') : message.get('source'), ? ConversationController.get(reaction.get('fromId'))
'private' : message.getContact();
);
const iconUrl = await sender.getNotificationIcon(); const iconUrl = await sender.getNotificationIcon();
@ -2805,42 +2810,33 @@
}, },
notifyTyping(options = {}) { notifyTyping(options = {}) {
const { const { isTyping, senderId, isMe, senderDevice } = options;
isTyping,
sender,
senderUuid,
senderId,
isMe,
senderDevice,
} = options;
// We don't do anything with typing messages from our other devices // We don't do anything with typing messages from our other devices
if (isMe) { if (isMe) {
return; return;
} }
const identifier = `${sender}.${senderDevice}`; const deviceId = `${senderId}.${senderDevice}`;
this.contactTypingTimers = this.contactTypingTimers || {}; this.contactTypingTimers = this.contactTypingTimers || {};
const record = this.contactTypingTimers[identifier]; const record = this.contactTypingTimers[deviceId];
if (record) { if (record) {
clearTimeout(record.timer); clearTimeout(record.timer);
} }
if (isTyping) { if (isTyping) {
this.contactTypingTimers[identifier] = this.contactTypingTimers[ this.contactTypingTimers[deviceId] = this.contactTypingTimers[
identifier deviceId
] || { ] || {
timestamp: Date.now(), timestamp: Date.now(),
sender,
senderId, senderId,
senderUuid,
senderDevice, senderDevice,
}; };
this.contactTypingTimers[identifier].timer = setTimeout( this.contactTypingTimers[deviceId].timer = setTimeout(
this.clearContactTypingTimer.bind(this, identifier), this.clearContactTypingTimer.bind(this, deviceId),
15 * 1000 15 * 1000
); );
if (!record) { if (!record) {
@ -2848,7 +2844,7 @@
this.trigger('change', this); this.trigger('change', this);
} }
} else { } else {
delete this.contactTypingTimers[identifier]; delete this.contactTypingTimers[deviceId];
if (record) { if (record) {
// User was previously typing, and is no longer. State change! // User was previously typing, and is no longer. State change!
this.trigger('change', this); this.trigger('change', this);
@ -2856,13 +2852,13 @@
} }
}, },
clearContactTypingTimer(identifier) { clearContactTypingTimer(deviceId) {
this.contactTypingTimers = this.contactTypingTimers || {}; this.contactTypingTimers = this.contactTypingTimers || {};
const record = this.contactTypingTimers[identifier]; const record = this.contactTypingTimers[deviceId];
if (record) { if (record) {
clearTimeout(record.timer); clearTimeout(record.timer);
delete this.contactTypingTimers[identifier]; delete this.contactTypingTimers[deviceId];
// User was previously typing, but timed out or we received message. State change! // User was previously typing, but timed out or we received message. State change!
this.trigger('change', this); this.trigger('change', this);
@ -2879,9 +2875,9 @@
* than just their id. * than just their id.
*/ */
initialize() { initialize() {
this._byE164 = {}; this._byE164 = Object.create(null);
this._byUuid = {}; this._byUuid = Object.create(null);
this._byGroupId = {}; this._byGroupId = Object.create(null);
this.on('idUpdated', (model, idProp, oldValue) => { this.on('idUpdated', (model, idProp, oldValue) => {
if (oldValue) { if (oldValue) {
if (idProp === 'e164') { if (idProp === 'e164') {
@ -2908,9 +2904,9 @@
reset(...args) { reset(...args) {
Backbone.Collection.prototype.reset.apply(this, args); Backbone.Collection.prototype.reset.apply(this, args);
this._byE164 = {}; this._byE164 = Object.create(null);
this._byUuid = {}; this._byUuid = Object.create(null);
this._byGroupId = {}; this._byGroupId = Object.create(null);
}, },
add(...models) { add(...models) {
@ -2918,12 +2914,22 @@
[].concat(res).forEach(model => { [].concat(res).forEach(model => {
const e164 = model.get('e164'); const e164 = model.get('e164');
if (e164) { if (e164) {
this._byE164[e164] = model; const existing = this._byE164[e164];
// Prefer the contact with both e164 and uuid
if (!existing || (existing && !existing.get('uuid'))) {
this._byE164[e164] = model;
}
} }
const uuid = model.get('uuid'); const uuid = model.get('uuid');
if (uuid) { if (uuid) {
this._byUuid[uuid] = model; const existing = this._byUuid[uuid];
// Prefer the contact with both e164 and uuid
if (!existing || (existing && !existing.get('e164'))) {
this._byUuid[uuid] = model;
}
} }
const groupId = model.get('groupId'); const groupId = model.get('groupId');

View file

@ -193,30 +193,20 @@
// Other top-level prop-generation // Other top-level prop-generation
getPropsForSearchResult() { getPropsForSearchResult() {
const sourceE164 = this.getSource(); const ourId = ConversationController.getOurConversationId();
const sourceUuid = this.getSourceUuid(); const sourceId = this.getContactId();
const fromContact = this.findAndFormatContact(sourceE164 || sourceUuid); const fromContact = this.findAndFormatContact(sourceId);
if ( if (ourId === sourceId) {
(sourceE164 && sourceE164 === this.OUR_NUMBER) ||
(sourceUuid && sourceUuid === this.OUR_UUID)
) {
fromContact.isMe = true; fromContact.isMe = true;
} }
const convo = this.getConversation(); const convo = this.getConversation();
let to = convo ? this.findAndFormatContact(convo.get('id')) : {}; const to = convo ? this.findAndFormatContact(convo.get('id')) : {};
if (convo && convo.isMe()) { if (to && convo && convo.isMe()) {
to.isMe = true; to.isMe = true;
} else if (
(sourceE164 && convo && sourceE164 === convo.get('e164')) ||
(sourceUuid && convo && sourceUuid === convo.get('uuid'))
) {
to = {
isMe: true,
};
} }
return { return {
@ -237,10 +227,10 @@
const unidentifiedLookup = ( const unidentifiedLookup = (
this.get('unidentifiedDeliveries') || [] this.get('unidentifiedDeliveries') || []
).reduce((accumulator, uuidOrE164) => { ).reduce((accumulator, identifier) => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
accumulator[ accumulator[
ConversationController.getConversationId(uuidOrE164) ConversationController.getConversationId(identifier)
] = true; ] = true;
return accumulator; return accumulator;
}, Object.create(null)); }, Object.create(null));
@ -249,7 +239,7 @@
// Older messages don't have the recipients included on the message, so we fall // Older messages don't have the recipients included on the message, so we fall
// back to the conversation's current recipients // back to the conversation's current recipients
const conversationIds = this.isIncoming() const conversationIds = this.isIncoming()
? [this.getContact().get('id')] ? [this.getContactId()]
: _.union( : _.union(
(this.get('sent_to') || []).map(id => (this.get('sent_to') || []).map(id =>
ConversationController.getConversationId(id) ConversationController.getConversationId(id)
@ -379,11 +369,11 @@
getPropsForUnsupportedMessage() { getPropsForUnsupportedMessage() {
const requiredVersion = this.get('requiredProtocolVersion'); const requiredVersion = this.get('requiredProtocolVersion');
const canProcessNow = this.CURRENT_PROTOCOL_VERSION >= requiredVersion; const canProcessNow = this.CURRENT_PROTOCOL_VERSION >= requiredVersion;
const phoneNumber = this.getSource(); const sourceId = this.getContactId();
return { return {
canProcessNow, canProcessNow,
contact: this.findAndFormatContact(phoneNumber), contact: this.findAndFormatContact(sourceId),
}; };
}, },
getPropsForTimerNotification() { getPropsForTimerNotification() {
@ -396,8 +386,14 @@
const timespan = Whisper.ExpirationTimerOptions.getName(expireTimer || 0); const timespan = Whisper.ExpirationTimerOptions.getName(expireTimer || 0);
const disabled = !expireTimer; const disabled = !expireTimer;
const sourceId = ConversationController.ensureContactIds({
e164: source,
uuid: sourceUuid,
});
const ourId = ConversationController.getOurConversationId();
const basicProps = { const basicProps = {
...this.findAndFormatContact(source), ...this.findAndFormatContact(sourceId),
type: 'fromOther', type: 'fromOther',
timespan, timespan,
disabled, disabled,
@ -408,7 +404,7 @@
...basicProps, ...basicProps,
type: 'fromSync', type: 'fromSync',
}; };
} else if (source === this.OUR_NUMBER || sourceUuid === this.OUR_UUID) { } else if (sourceId && sourceId === ourId) {
return { return {
...basicProps, ...basicProps,
type: 'fromMe', type: 'fromMe',
@ -430,12 +426,12 @@
getPropsForVerificationNotification() { getPropsForVerificationNotification() {
const type = this.get('verified') ? 'markVerified' : 'markNotVerified'; const type = this.get('verified') ? 'markVerified' : 'markNotVerified';
const isLocal = this.get('local'); const isLocal = this.get('local');
const phoneNumber = this.get('verifiedChanged'); const identifier = this.get('verifiedChanged');
return { return {
type, type,
isLocal, isLocal,
contact: this.findAndFormatContact(phoneNumber), contact: this.findAndFormatContact(identifier),
}; };
}, },
getPropsForGroupNotification() { getPropsForGroupNotification() {
@ -460,7 +456,7 @@
Array.isArray(groupUpdate.joined) Array.isArray(groupUpdate.joined)
? groupUpdate.joined ? groupUpdate.joined
: [groupUpdate.joined], : [groupUpdate.joined],
phoneNumber => this.findAndFormatContact(phoneNumber) identifier => this.findAndFormatContact(identifier)
), ),
}); });
} }
@ -477,7 +473,7 @@
Array.isArray(groupUpdate.left) Array.isArray(groupUpdate.left)
? groupUpdate.left ? groupUpdate.left
: [groupUpdate.left], : [groupUpdate.left],
phoneNumber => this.findAndFormatContact(phoneNumber) identifier => this.findAndFormatContact(identifier)
), ),
}); });
} }
@ -495,9 +491,8 @@
}); });
} }
const sourceE164 = this.getSource(); const sourceId = this.getContactId();
const sourceUuid = this.getSourceUuid(); const from = this.findAndFormatContact(sourceId);
const from = this.findAndFormatContact(sourceE164 || sourceUuid);
return { return {
from, from,
@ -537,10 +532,9 @@
.map(attachment => this.getPropsForAttachment(attachment)); .map(attachment => this.getPropsForAttachment(attachment));
}, },
getPropsForMessage() { getPropsForMessage() {
const sourceE164 = this.getSource(); const sourceId = this.getContactId();
const sourceUuid = this.getSourceUuid(); const contact = this.findAndFormatContact(sourceId);
const contact = this.findAndFormatContact(sourceE164 || sourceUuid); const contactModel = this.findContact(sourceId);
const contactModel = this.findContact(sourceE164 || sourceUuid);
const authorColor = contactModel ? contactModel.getColor() : null; const authorColor = contactModel ? contactModel.getColor() : null;
const authorAvatarPath = contactModel const authorAvatarPath = contactModel
@ -774,7 +768,13 @@
referencedMessageNotFound, referencedMessageNotFound,
} = quote; } = quote;
const contact = const contact =
author && ConversationController.get(author || authorUuid); (author || authorUuid) &&
ConversationController.get(
ConversationController.ensureContactIds({
e164: author,
uuid: authorUuid,
})
);
const authorColor = contact ? contact.getColor() : 'grey'; const authorColor = contact ? contact.getColor() : 'grey';
const authorPhoneNumber = format(author, { const authorPhoneNumber = format(author, {
@ -810,17 +810,18 @@
const e164 = conversation.get('e164'); const e164 = conversation.get('e164');
const uuid = conversation.get('uuid'); const uuid = conversation.get('uuid');
const conversationId = conversation.get('id');
const readBy = this.get('read_by') || []; const readBy = this.get('read_by') || [];
if (includesAny(readBy, identifier, e164, uuid)) { if (includesAny(readBy, conversationId, e164, uuid)) {
return 'read'; return 'read';
} }
const deliveredTo = this.get('delivered_to') || []; const deliveredTo = this.get('delivered_to') || [];
if (includesAny(deliveredTo, identifier, e164, uuid)) { if (includesAny(deliveredTo, conversationId, e164, uuid)) {
return 'delivered'; return 'delivered';
} }
const sentTo = this.get('sent_to') || []; const sentTo = this.get('sent_to') || [];
if (includesAny(sentTo, identifier, e164, uuid)) { if (includesAny(sentTo, conversationId, e164, uuid)) {
return 'sent'; return 'sent';
} }
@ -1220,19 +1221,22 @@
return this.OUR_UUID; return this.OUR_UUID;
}, },
getContact() { getContactId() {
const source = this.getSource(); const source = this.getSource();
const sourceUuid = this.getSourceUuid(); const sourceUuid = this.getSourceUuid();
if (!source && !sourceUuid) { if (!source && !sourceUuid) {
return null; return ConversationController.getOurConversationId();
} }
const contactId = ConversationController.ensureContactIds({ return ConversationController.ensureContactIds({
e164: source, e164: source,
uuid: sourceUuid, uuid: sourceUuid,
}); });
return ConversationController.get(contactId, 'private'); },
getContact() {
const id = this.getContactId();
return ConversationController.get(id);
}, },
isOutgoing() { isOutgoing() {
return this.get('type') === 'outgoing'; return this.get('type') === 'outgoing';
@ -1395,7 +1399,7 @@
let recipients = _.intersection(intendedRecipients, currentRecipients); let recipients = _.intersection(intendedRecipients, currentRecipients);
recipients = _.without(recipients, successfulRecipients).map(id => { recipients = _.without(recipients, successfulRecipients).map(id => {
const c = ConversationController.get(id); const c = ConversationController.get(id);
return c.get('uuid') || c.get('e164'); return c.getSendTarget();
}); });
if (!recipients.length) { if (!recipients.length) {
@ -1699,7 +1703,7 @@
try { try {
this.set({ this.set({
// These are the same as a normal send() // These are the same as a normal send()
sent_to: [conv.get('uuid') || conv.get('e164')], sent_to: [conv.getSendTarget()],
sent: true, sent: true,
expirationStartTimestamp: Date.now(), expirationStartTimestamp: Date.now(),
}); });
@ -1709,8 +1713,8 @@
unidentifiedDeliveries: result ? result.unidentifiedDeliveries : null, unidentifiedDeliveries: result ? result.unidentifiedDeliveries : null,
// These are unique to a Note to Self message - immediately read/delivered // These are unique to a Note to Self message - immediately read/delivered
delivered_to: [this.OUR_UUID || this.OUR_NUMBER], delivered_to: [ConversationController.getOurConversationId()],
read_by: [this.OUR_UUID || this.OUR_NUMBER], read_by: [ConversationController.getOurConversationId()],
}); });
} catch (result) { } catch (result) {
const errors = (result && result.errors) || [ const errors = (result && result.errors) || [
@ -2004,20 +2008,20 @@
return message; return message;
} }
const { attachments, id, author } = quote; const { attachments, id, author, authorUuid } = quote;
const firstAttachment = attachments[0]; const firstAttachment = attachments[0];
const authorConversationId = ConversationController.ensureContactIds({
e164: author,
uuid: authorUuid,
});
const collection = await window.Signal.Data.getMessagesBySentAt(id, { const collection = await window.Signal.Data.getMessagesBySentAt(id, {
MessageCollection: Whisper.MessageCollection, MessageCollection: Whisper.MessageCollection,
}); });
const found = collection.find(item => { const found = collection.find(item => {
const messageAuthor = item.getContact(); const messageAuthorId = item.getContactId();
return ( return authorConversationId === messageAuthorId;
messageAuthor &&
ConversationController.getConversationId(author) ===
messageAuthor.get('id')
);
}); });
if (!found) { if (!found) {
@ -2119,10 +2123,7 @@
const source = message.get('source'); const source = message.get('source');
const sourceUuid = message.get('sourceUuid'); const sourceUuid = message.get('sourceUuid');
const type = message.get('type'); const type = message.get('type');
let conversationId = message.get('conversationId'); const conversationId = message.get('conversationId');
if (initialMessage.group) {
conversationId = initialMessage.group.id;
}
const GROUP_TYPES = textsecure.protobuf.GroupContext.Type; const GROUP_TYPES = textsecure.protobuf.GroupContext.Type;
const conversation = ConversationController.get(conversationId); const conversation = ConversationController.get(conversationId);
@ -2392,10 +2393,13 @@
if (conversation.get('left')) { if (conversation.get('left')) {
window.log.warn('re-added to a left group'); window.log.warn('re-added to a left group');
attributes.left = false; attributes.left = false;
conversation.set({ addedBy: message.getContact().get('id') }); conversation.set({ addedBy: message.getContactId() });
} }
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) { } else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
const sender = ConversationController.get(source || sourceUuid); const sender = ConversationController.ensureContactIds({
e164: source,
uuid: sourceUuid,
});
const inGroup = Boolean( const inGroup = Boolean(
sender && sender &&
(conversation.get('members') || []).includes(sender.id) (conversation.get('members') || []).includes(sender.id)
@ -2453,6 +2457,7 @@
message.set({ message.set({
expirationTimerUpdate: { expirationTimerUpdate: {
source, source,
sourceUuid,
expireTimer: dataMessage.expireTimer, expireTimer: dataMessage.expireTimer,
}, },
}); });
@ -2567,9 +2572,7 @@
e164: source, e164: source,
uuid: sourceUuid, uuid: sourceUuid,
}); });
ConversationController.get(localId, 'private').setProfileKey( ConversationController.get(localId).setProfileKey(profileKey);
profileKey
);
} }
} }

View file

@ -15,15 +15,8 @@ let scheduleNext = null;
// do not support unidentified delivery. // do not support unidentified delivery.
function refreshOurProfile() { function refreshOurProfile() {
window.log.info('refreshOurProfile'); window.log.info('refreshOurProfile');
const ourNumber = textsecure.storage.user.getNumber(); const ourId = ConversationController.getOurConversationId();
const ourUuid = textsecure.storage.user.getUuid(); const conversation = ConversationController.get(ourId);
const ourId = ConversationController.ensureContactIds({
e164: ourNumber,
uuid: ourUuid,
});
const conversation = ConversationController.get(ourId, 'private');
conversation.updateUuid(ourUuid);
conversation.updateE164(ourNumber);
conversation.getProfiles(); conversation.getProfiles();
} }

View file

@ -3,6 +3,9 @@
const { bindActionCreators } = require('redux'); const { bindActionCreators } = require('redux');
const Backbone = require('../../ts/backbone'); const Backbone = require('../../ts/backbone');
const Crypto = require('../../ts/Crypto'); const Crypto = require('../../ts/Crypto');
const {
start: conversationControllerStart,
} = require('../../ts/ConversationController');
const Data = require('../../ts/sql/Client').default; const Data = require('../../ts/sql/Client').default;
const Emojis = require('./emojis'); const Emojis = require('./emojis');
const EmojiLib = require('../../ts/components/emoji/lib'); const EmojiLib = require('../../ts/components/emoji/lib');
@ -357,6 +360,7 @@ exports.setup = (options = {}) => {
Backbone, Backbone,
Components, Components,
Crypto, Crypto,
conversationControllerStart,
Data, Data,
Emojis, Emojis,
EmojiLib, EmojiLib,

View file

@ -28,9 +28,10 @@
const reactionsBySource = this.filter(re => { const reactionsBySource = this.filter(re => {
const mcid = message.get('conversationId'); const mcid = message.get('conversationId');
const recid = ConversationController.getConversationId( const recid = ConversationController.ensureContactIds({
re.get('targetAuthorE164') || re.get('targetAuthorUuid') e164: re.get('targetAuthorE164'),
); uuid: re.get('targetAuthorUuid'),
});
const mTime = message.get('sent_at'); const mTime = message.get('sent_at');
const rTime = re.get('targetTimestamp'); const rTime = re.get('targetTimestamp');
return mcid === recid && mTime === rTime; return mcid === recid && mTime === rTime;
@ -47,9 +48,10 @@
async onReaction(reaction) { async onReaction(reaction) {
try { try {
const targetConversation = await ConversationController.getConversationForTargetMessage( const targetConversation = await ConversationController.getConversationForTargetMessage(
// Do not use ensureContactIds here since maliciously malformed ConversationController.ensureContactIds({
// reactions from clients could cause issues e164: reaction.get('targetAuthorE164'),
reaction.get('targetAuthorE164') || reaction.get('targetAuthorUuid'), uuid: reaction.get('targetAuthorUuid'),
}),
reaction.get('targetTimestamp') reaction.get('targetTimestamp')
); );
if (!targetConversation) { if (!targetConversation) {
@ -85,10 +87,10 @@
} }
const mcid = contact.get('id'); const mcid = contact.get('id');
const recid = ConversationController.getConversationId( const recid = ConversationController.ensureContactIds({
reaction.get('targetAuthorE164') || e164: reaction.get('targetAuthorE164'),
reaction.get('targetAuthorUuid') uuid: reaction.get('targetAuthorUuid'),
); });
return mcid === recid; return mcid === recid;
}); });

View file

@ -2058,7 +2058,7 @@
await contact.setApproved(); await contact.setApproved();
} }
message.resend(contact.get('uuid') || contact.get('e164')); message.resend(contact.getSendTarget());
}, },
}); });

View file

@ -84,6 +84,13 @@
model: { window: options.window }, model: { window: options.window },
}); });
Whisper.events.on('refreshConversation', ({ oldId, newId }) => {
const convo = this.conversation_stack.lastConversation;
if (convo && convo.get('id') === oldId) {
this.conversation_stack.open(newId);
}
});
if (!options.initialLoadComplete) { if (!options.initialLoadComplete) {
this.appLoadingScreen = new Whisper.AppLoadingScreen(); this.appLoadingScreen = new Whisper.AppLoadingScreen();
this.appLoadingScreen.render(); this.appLoadingScreen.render();

View file

@ -30,7 +30,6 @@
<script type="text/javascript" src="../../js/models/blockedNumbers.js" data-cover></script> <script type="text/javascript" src="../../js/models/blockedNumbers.js" data-cover></script>
<script type="text/javascript" src="../../js/models/messages.js" data-cover></script> <script type="text/javascript" src="../../js/models/messages.js" data-cover></script>
<script type="text/javascript" src="../../js/models/conversations.js" data-cover></script> <script type="text/javascript" src="../../js/models/conversations.js" data-cover></script>
<script type="text/javascript" src="../../js/conversation_controller.js" data-cover></script>
<script type="text/javascript" src="errors_test.js"></script> <script type="text/javascript" src="errors_test.js"></script>
<script type="text/javascript" src="helpers_test.js"></script> <script type="text/javascript" src="helpers_test.js"></script>
@ -51,6 +50,7 @@
<!-- Uncomment to start tests without code coverage enabled --> <!-- Uncomment to start tests without code coverage enabled -->
<script type="text/javascript"> <script type="text/javascript">
mocha.run(); mocha.run();
window.Signal.conversationControllerStart();
</script> </script>
</body> </body>
</html> </html>

View file

@ -64,8 +64,6 @@
"dependencies": { "dependencies": {
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#b10f232fac62ba7f8775c9e086bb5558fe7d948b", "@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#b10f232fac62ba7f8775c9e086bb5558fe7d948b",
"@sindresorhus/is": "0.8.0", "@sindresorhus/is": "0.8.0",
"@types/node-fetch": "2.5.5",
"@types/websocket": "1.0.0",
"array-move": "2.1.0", "array-move": "2.1.0",
"backbone": "1.3.3", "backbone": "1.3.3",
"blob-util": "1.3.0", "blob-util": "1.3.0",
@ -172,13 +170,14 @@
"@types/got": "9.4.1", "@types/got": "9.4.1",
"@types/history": "4.7.2", "@types/history": "4.7.2",
"@types/html-webpack-plugin": "3.2.1", "@types/html-webpack-plugin": "3.2.1",
"@types/jquery": "3.3.29", "@types/jquery": "3.5.0",
"@types/js-yaml": "3.12.0", "@types/js-yaml": "3.12.0",
"@types/linkify-it": "2.1.0", "@types/linkify-it": "2.1.0",
"@types/lodash": "4.14.106", "@types/lodash": "4.14.106",
"@types/memoizee": "0.4.2", "@types/memoizee": "0.4.2",
"@types/mkdirp": "0.5.2", "@types/mkdirp": "0.5.2",
"@types/mocha": "5.0.0", "@types/mocha": "5.0.0",
"@types/node-fetch": "2.5.5",
"@types/normalize-path": "3.0.0", "@types/normalize-path": "3.0.0",
"@types/pify": "3.0.2", "@types/pify": "3.0.2",
"@types/qs": "6.5.1", "@types/qs": "6.5.1",
@ -197,9 +196,11 @@
"@types/storybook__addon-actions": "3.4.3", "@types/storybook__addon-actions": "3.4.3",
"@types/storybook__addon-knobs": "5.0.3", "@types/storybook__addon-knobs": "5.0.3",
"@types/storybook__react": "4.0.2", "@types/storybook__react": "4.0.2",
"@types/underscore": "1.10.3",
"@types/uuid": "3.4.4", "@types/uuid": "3.4.4",
"@types/webpack": "4.39.0", "@types/webpack": "4.39.0",
"@types/webpack-dev-server": "3.1.7", "@types/webpack-dev-server": "3.1.7",
"@types/websocket": "1.0.0",
"arraybuffer-loader": "1.0.3", "arraybuffer-loader": "1.0.3",
"asar": "0.14.0", "asar": "0.14.0",
"babel-core": "7.0.0-bridge.0", "babel-core": "7.0.0-bridge.0",

View file

@ -345,7 +345,6 @@
<script type="text/javascript" src="../js/models/messages.js" data-cover></script> <script type="text/javascript" src="../js/models/messages.js" data-cover></script>
<script type="text/javascript" src="../js/models/conversations.js" data-cover></script> <script type="text/javascript" src="../js/models/conversations.js" data-cover></script>
<script type="text/javascript" src="../js/models/blockedNumbers.js" data-cover></script> <script type="text/javascript" src="../js/models/blockedNumbers.js" data-cover></script>
<script type="text/javascript" src="../js/conversation_controller.js" data-cover></script>
<script type="text/javascript" src="../js/message_controller.js" data-cover></script> <script type="text/javascript" src="../js/message_controller.js" data-cover></script>
<script type="text/javascript" src="../js/keychange_listener.js" data-cover></script> <script type="text/javascript" src="../js/keychange_listener.js" data-cover></script>
<script type='text/javascript' src='../js/expiring_messages.js' data-cover></script> <script type='text/javascript' src='../js/expiring_messages.js' data-cover></script>
@ -393,6 +392,7 @@
<!-- Uncomment to start tests without code coverage enabled --> <!-- Uncomment to start tests without code coverage enabled -->
<script type="text/javascript"> <script type="text/javascript">
window.Signal.conversationControllerStart();
mocha.run(); mocha.run();
</script> </script>
</body> </body>

View file

@ -0,0 +1,675 @@
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
import { debounce, reduce, uniq, without } from 'lodash';
import dataInterface from './sql/Client';
import {
ConversationModelCollectionType,
ConversationModelType,
ConversationTypeType,
} from './model-types.d';
import { SendOptionsType } from './textsecure/SendMessage';
const {
getAllConversations,
getAllGroupsInvolvingId,
getMessagesBySentAt,
migrateConversationMessages,
removeConversation,
saveConversation,
updateConversation,
} = dataInterface;
// We have to run this in background.js, after all backbone models and collections on
// Whisper.* have been created. Once those are in typescript we can use more reasonable
// require statements for referencing these things, giving us more flexibility here.
export function start() {
const conversations = new window.Whisper.ConversationCollection();
// This class is entirely designed to keep the app title, badge and tray icon updated.
// In the future it could listen to redux changes and do its updates there.
const inboxCollection = new (window.Backbone.Collection.extend({
initialize() {
this.listenTo(conversations, 'add change:active_at', this.addActive);
this.listenTo(conversations, 'reset', () => this.reset([]));
this.on(
'add remove change:unreadCount',
debounce(this.updateUnreadCount.bind(this), 1000)
);
},
addActive(model: ConversationModelType) {
if (model.get('active_at')) {
this.add(model);
} else {
this.remove(model);
}
},
updateUnreadCount() {
const newUnreadCount = reduce(
this.map((m: ConversationModelType) => m.get('unreadCount')),
(item: number, memo: number) => (item || 0) + memo,
0
);
window.storage.put('unreadCount', newUnreadCount);
if (newUnreadCount > 0) {
window.setBadgeCount(newUnreadCount);
window.document.title = `${window.getTitle()} (${newUnreadCount})`;
} else {
window.setBadgeCount(0);
window.document.title = window.getTitle();
}
window.updateTrayIcon(newUnreadCount);
},
}))();
window.getInboxCollection = () => inboxCollection;
window.getConversations = () => conversations;
window.ConversationController = new ConversationController(conversations);
}
export class ConversationController {
_initialFetchComplete: boolean | undefined;
_initialPromise: Promise<void> = Promise.resolve();
_conversations: ConversationModelCollectionType;
constructor(conversations?: ConversationModelCollectionType) {
if (!conversations) {
throw new Error('ConversationController: need conversation collection!');
}
this._conversations = conversations;
}
get(id?: string | null): ConversationModelType | undefined {
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
// This function takes null just fine. Backbone typings are too restrictive.
return this._conversations.get(id as string);
}
// Needed for some model setup which happens during the initial fetch() call below
getUnsafe(id: string) {
return this._conversations.get(id);
}
dangerouslyCreateAndAdd(attributes: Partial<ConversationModelType>) {
return this._conversations.add(attributes);
}
getOrCreate(
identifier: string,
type: ConversationTypeType,
additionalInitialProps = {}
) {
if (typeof identifier !== 'string') {
throw new TypeError("'id' must be a string");
}
if (type !== 'private' && type !== 'group') {
throw new TypeError(
`'type' must be 'private' or 'group'; got: '${type}'`
);
}
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
let conversation = this._conversations.get(identifier);
if (conversation) {
return conversation;
}
const id = window.getGuid();
if (type === 'group') {
conversation = this._conversations.add({
id,
uuid: null,
e164: null,
groupId: identifier,
type,
version: 2,
...additionalInitialProps,
});
} else if (window.isValidGuid(identifier)) {
conversation = this._conversations.add({
id,
uuid: identifier,
e164: null,
groupId: null,
type,
version: 2,
...additionalInitialProps,
});
} else {
conversation = this._conversations.add({
id,
uuid: null,
e164: identifier,
groupId: null,
type,
version: 2,
...additionalInitialProps,
});
}
const create = async () => {
if (!conversation.isValid()) {
const validationError = conversation.validationError || {};
window.log.error(
'Contact is not valid. Not saving, but adding to collection:',
conversation.idForLogging(),
validationError.stack
);
return conversation;
}
try {
await saveConversation(conversation.attributes);
} catch (error) {
window.log.error(
'Conversation save failed! ',
identifier,
type,
'Error:',
error && error.stack ? error.stack : error
);
throw error;
}
return conversation;
};
conversation.initialPromise = create();
return conversation;
}
async getOrCreateAndWait(
id: string,
type: ConversationTypeType,
additionalInitialProps = {}
) {
return this._initialPromise.then(async () => {
const conversation = this.getOrCreate(id, type, additionalInitialProps);
if (conversation) {
return conversation.initialPromise.then(() => conversation);
}
return Promise.reject(
new Error('getOrCreateAndWait: did not get conversation')
);
});
}
getConversationId(address: string) {
if (!address) {
return null;
}
const [id] = window.textsecure.utils.unencodeNumber(address);
const conv = this.get(id);
if (conv) {
return conv.get('id');
}
return null;
}
getOurConversationId() {
const e164 = window.textsecure.storage.user.getNumber();
const uuid = window.textsecure.storage.user.getUuid();
return this.ensureContactIds({ e164, uuid, highTrust: true });
}
/**
* Given a UUID and/or an E164, resolves to a string representing the local
* database id of the given contact. It may create new contacts, and it may merge
* contacts.
*
* lowTrust = uuid/e164 pairing came from source like GroupV1 member list
* highTrust = uuid/e164 pairing came from source like CDS
*/
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
ensureContactIds({
e164,
uuid,
highTrust,
}: {
e164?: string;
uuid?: string;
highTrust?: boolean;
}) {
// Check for at least one parameter being provided. This is necessary
// because this path can be called on startup to resolve our own ID before
// our phone number or UUID are known. The existing behavior in these
// cases can handle a returned `undefined` id, so we do that.
const normalizedUuid = uuid ? uuid.toLowerCase() : undefined;
const identifier = normalizedUuid || e164;
if ((!e164 && !uuid) || !identifier) {
return undefined;
}
const convoE164 = this.get(e164);
const convoUuid = this.get(normalizedUuid);
// 1. Handle no match at all
if (!convoE164 && !convoUuid) {
window.log.info(
'ensureContactIds: Creating new contact, no matches found'
);
const newConvo = this.getOrCreate(identifier, 'private');
if (highTrust && e164) {
newConvo.updateE164(e164);
}
if (normalizedUuid) {
newConvo.updateUuid(normalizedUuid);
}
if (highTrust && e164 && normalizedUuid) {
updateConversation(newConvo.attributes);
}
return newConvo.get('id');
// 2. Handle match on only E164
} else if (convoE164 && !convoUuid) {
const haveUuid = Boolean(normalizedUuid);
window.log.info(
`ensureContactIds: e164-only match found (have UUID: ${haveUuid})`
);
// If we are only searching based on e164 anyway, then return the first result
if (!normalizedUuid) {
return convoE164.get('id');
}
// Fill in the UUID for an e164-only contact
if (normalizedUuid && !convoE164.get('uuid')) {
if (highTrust) {
window.log.info('ensureContactIds: Adding UUID to e164-only match');
convoE164.updateUuid(normalizedUuid);
updateConversation(convoE164.attributes);
}
return convoE164.get('id');
}
window.log.info(
'ensureContactIds: e164 already had UUID, creating a new contact'
);
// If existing e164 match already has UUID, create a new contact...
const newConvo = this.getOrCreate(normalizedUuid, 'private');
if (highTrust) {
window.log.info(
'ensureContactIds: Moving e164 from old contact to new'
);
// Remove the e164 from the old contact...
convoE164.set({ e164: undefined });
updateConversation(convoE164.attributes);
// ...and add it to the new one.
newConvo.updateE164(e164);
updateConversation(newConvo.attributes);
}
return newConvo.get('id');
// 3. Handle match on only UUID
} else if (!convoE164 && convoUuid) {
window.log.info(
`ensureContactIds: UUID-only match found (have e164: ${Boolean(e164)})`
);
if (e164 && highTrust) {
convoUuid.updateE164(e164);
updateConversation(convoUuid.attributes);
}
return convoUuid.get('id');
}
// For some reason, TypeScript doesn't believe that we can trust that these two values
// are truthy by this point. So we'll throw if we get there.
if (!convoE164 || !convoUuid) {
throw new Error('ensureContactIds: convoE164 or convoUuid are falsey!');
}
// Now, we know that we have a match for both e164 and uuid checks
if (convoE164 === convoUuid) {
return convoUuid.get('id');
}
if (highTrust) {
// Conflict: If e164 match already has a UUID, we remove its e164.
if (convoE164.get('uuid') && convoE164.get('uuid') !== normalizedUuid) {
window.log.info(
'ensureContactIds: e164 match had different UUID than incoming pair, removing its e164.'
);
// Remove the e164 from the old contact...
convoE164.set({ e164: undefined });
updateConversation(convoE164.attributes);
// ...and add it to the new one.
convoUuid.updateE164(e164);
updateConversation(convoUuid.attributes);
return convoUuid.get('id');
}
window.log.warn(
`ensureContactIds: Found a split contact - UUID ${normalizedUuid} and E164 ${e164}. Merging.`
);
// Conflict: If e164 match has no UUID, we merge. We prefer the UUID match.
// Note: no await here, we want to keep this function synchronous
this.combineContacts(convoUuid, convoE164)
.then(() => {
// If the old conversation was currently displayed, we load the new one
window.Whisper.events.trigger('refreshConversation', {
newId: convoUuid.get('id'),
oldId: convoE164.get('id'),
});
})
.catch(error => {
const errorText = error && error.stack ? error.stack : error;
window.log.warn(
`ensureContactIds error combining contacts: ${errorText}`
);
});
}
return convoUuid.get('id');
}
async checkForConflicts() {
window.log.info('checkForConflicts: starting...');
const byUuid = Object.create(null);
const byE164 = Object.create(null);
// We iterate from the oldest conversations to the newest. This allows us, in a
// conflict case, to keep the one with activity the most recently.
const models = [...this._conversations.models.reverse()];
const max = models.length;
for (let i = 0; i < max; i += 1) {
const conversation = models[i];
const uuid = conversation.get('uuid');
const e164 = conversation.get('e164');
if (uuid) {
const existing = byUuid[uuid];
if (!existing) {
byUuid[uuid] = conversation;
} else {
window.log.warn(
`checkForConflicts: Found conflict with uuid ${uuid}`
);
// Keep the newer one if it has an e164, otherwise keep existing
if (conversation.get('e164')) {
// Keep new one
// eslint-disable-next-line no-await-in-loop
await this.combineContacts(conversation, existing);
byUuid[uuid] = conversation;
} else {
// Keep existing - note that this applies if neither had an e164
// eslint-disable-next-line no-await-in-loop
await this.combineContacts(existing, conversation);
}
}
}
if (e164) {
const existing = byE164[e164];
if (!existing) {
byE164[e164] = conversation;
} else {
// If we have two contacts with the same e164 but different truthy UUIDs, then
// we'll delete the e164 on the older one
if (
conversation.get('uuid') &&
existing.get('uuid') &&
conversation.get('uuid') !== existing.get('uuid')
) {
window.log.warn(
`checkForConflicts: Found two matches on e164 ${e164} with different truthy UUIDs. Dropping e164 on older.`
);
existing.set({ e164: undefined });
updateConversation(existing.attributes);
byE164[e164] = conversation;
// eslint-disable-next-line no-continue
continue;
}
window.log.warn(
`checkForConflicts: Found conflict with e164 ${e164}`
);
// Keep the newer one if it has a UUID, otherwise keep existing
if (conversation.get('uuid')) {
// Keep new one
// eslint-disable-next-line no-await-in-loop
await this.combineContacts(conversation, existing);
byE164[e164] = conversation;
} else {
// Keep existing - note that this applies if neither had a UUID
// eslint-disable-next-line no-await-in-loop
await this.combineContacts(existing, conversation);
}
}
}
}
window.log.info('checkForConflicts: complete!');
}
async combineContacts(
current: ConversationModelType,
obsolete: ConversationModelType
) {
const obsoleteId = obsolete.get('id');
const currentId = current.get('id');
window.log.warn('combineContacts: Combining two conversations', {
obsolete: obsoleteId,
current: currentId,
});
if (!current.get('profileKey') && obsolete.get('profileKey')) {
window.log.warn(
'combineContacts: Copying profile key from old to new contact'
);
await current.setProfileKey(obsolete.get('profileKey'));
}
window.log.warn(
'combineContacts: Delete all sessions tied to old conversationId'
);
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
obsoleteId
);
await Promise.all(
deviceIds.map(async deviceId => {
await window.textsecure.storage.protocol.removeSession(
`${obsoleteId}.${deviceId}`
);
})
);
window.log.warn(
'combineContacts: Delete all identity information tied to old conversationId'
);
await window.textsecure.storage.protocol.removeIdentityKey(obsoleteId);
window.log.warn(
'combineContacts: Ensure that all V1 groups have new conversationId instead of old'
);
const groups = await this.getAllGroupsInvolvingId(obsoleteId);
groups.forEach(group => {
const members = group.get('members');
const withoutObsolete = without(members, obsoleteId);
const currentAdded = uniq([...withoutObsolete, currentId]);
group.set({
members: currentAdded,
});
updateConversation(group.attributes);
});
// Note: we explicitly don't want to update V2 groups
window.log.warn(
'combineContacts: Delete the obsolete conversation from the database'
);
await removeConversation(obsoleteId, {
Conversation: window.Whisper.Conversation,
});
window.log.warn('combineContacts: Update messages table');
await migrateConversationMessages(obsoleteId, currentId);
window.log.warn(
'combineContacts: Eliminate old conversation from ConversationController lookups'
);
this._conversations.remove(obsolete);
this.regenerateLookups();
window.log.warn('combineContacts: Complete!', {
obsolete: obsoleteId,
current: currentId,
});
}
regenerateLookups() {
const models = [...this._conversations.models];
this.reset();
this._conversations.add(models);
// We force the initial fetch to be true
this._initialFetchComplete = true;
}
/**
* Given a groupId and optional additional initialization properties,
* ensures the existence of a group conversation and returns a string
* representing the local database ID of the group conversation.
*/
ensureGroup(groupId: string, additionalInitProps = {}) {
return this.getOrCreate(groupId, 'group', additionalInitProps).get('id');
}
/**
* Given certain metadata about a message (an identifier of who wrote the
* message and the sent_at timestamp of the message) returns the
* conversation the message belongs to OR null if a conversation isn't
* found.
*/
async getConversationForTargetMessage(
targetFromId: string,
targetTimestamp: number
) {
const messages = await getMessagesBySentAt(targetTimestamp, {
MessageCollection: window.Whisper.MessageCollection,
});
const targetMessage = messages.find(m => {
const contact = m.getContact();
if (!contact) {
return false;
}
const mcid = contact.get('id');
return mcid === targetFromId;
});
if (targetMessage) {
return targetMessage.getConversation();
}
return null;
}
prepareForSend(
id: string,
options?: any
): {
wrap: (promise: Promise<any>) => Promise<void>;
sendOptions: SendOptionsType | undefined;
} {
// id is any valid conversation identifier
const conversation = this.get(id);
const sendOptions = conversation
? conversation.getSendOptions(options)
: undefined;
const wrap = conversation
? conversation.wrapSend.bind(conversation)
: async (promise: Promise<any>) => promise;
return { wrap, sendOptions };
}
async getAllGroupsInvolvingId(
conversationId: string
): Promise<Array<ConversationModelType>> {
const groups = await getAllGroupsInvolvingId(conversationId, {
ConversationCollection: window.Whisper.ConversationCollection,
});
return groups.map(group => this._conversations.add(group));
}
async loadPromise() {
return this._initialPromise;
}
reset() {
this._initialPromise = Promise.resolve();
this._initialFetchComplete = false;
this._conversations.reset([]);
}
async load() {
window.log.info('ConversationController: starting initial fetch');
if (this._conversations.length) {
throw new Error('ConversationController: Already loaded!');
}
const load = async () => {
try {
const collection = await getAllConversations({
ConversationCollection: window.Whisper.ConversationCollection,
});
this._conversations.add(collection.models);
this._initialFetchComplete = true;
await Promise.all(
this._conversations.map(async conversation => {
if (!conversation.get('lastMessage')) {
await conversation.updateLastMessage();
}
// In case a too-large draft was saved to the database
const draft = conversation.get('draft');
if (draft && draft.length > MAX_MESSAGE_BODY_LENGTH) {
conversation.set({
draft: draft.slice(0, MAX_MESSAGE_BODY_LENGTH),
});
updateConversation(conversation.attributes);
}
})
);
window.log.info('ConversationController: done with initial fetch');
} catch (error) {
window.log.error(
'ConversationController: initial fetch failed',
error && error.stack ? error.stack : error
);
throw error;
}
};
this._initialPromise = load();
return this._initialPromise;
}
}

5
ts/model-types.d.ts vendored
View file

@ -1,7 +1,9 @@
import * as Backbone from 'backbone'; import * as Backbone from 'backbone';
import { ColorType, LocalizerType } from './types/Util'; import { ColorType, LocalizerType } from './types/Util';
import { SendOptionsType } from './textsecure/SendMessage';
import { ConversationType } from './state/ducks/conversations'; import { ConversationType } from './state/ducks/conversations';
import { CallingClass, CallHistoryDetailsType } from './services/calling';
import { SendOptionsType } from './textsecure/SendMessage';
import { SyncMessageClass } from './textsecure.d'; import { SyncMessageClass } from './textsecure.d';
interface ModelAttributesInterface { interface ModelAttributesInterface {
@ -74,6 +76,7 @@ declare class ConversationModelType extends Backbone.Model<
cachedProps: ConversationType; cachedProps: ConversationType;
initialPromise: Promise<any>; initialPromise: Promise<any>;
addCallHistory(details: CallHistoryDetailsType): void;
applyMessageRequestResponse( applyMessageRequestResponse(
response: number, response: number,
options?: { fromSync: boolean } options?: { fromSync: boolean }

View file

@ -16,7 +16,7 @@ import {
CallDetailsType, CallDetailsType,
} from '../state/ducks/calling'; } from '../state/ducks/calling';
import { CallingMessageClass, EnvelopeClass } from '../textsecure.d'; import { CallingMessageClass, EnvelopeClass } from '../textsecure.d';
import { ConversationType } from '../window.d'; import { ConversationModelType } from '../model-types.d';
import is from '@sindresorhus/is'; import is from '@sindresorhus/is';
export { export {
@ -72,7 +72,7 @@ export class CallingClass {
} }
async startOutgoingCall( async startOutgoingCall(
conversation: ConversationType, conversation: ConversationModelType,
isVideoCall: boolean isVideoCall: boolean
) { ) {
if (!this.uxActions) { if (!this.uxActions) {
@ -218,7 +218,14 @@ export class CallingClass {
message: CallingMessageClass message: CallingMessageClass
): Promise<boolean> { ): Promise<boolean> {
const conversation = window.ConversationController.get(remoteUserId); const conversation = window.ConversationController.get(remoteUserId);
const sendOptions = conversation ? conversation.getSendOptions() : {}; const sendOptions = conversation
? conversation.getSendOptions()
: undefined;
if (!window.textsecure.messaging) {
window.log.warn('handleOutgoingSignaling() returning false; offline');
return false;
}
try { try {
await window.textsecure.messaging.sendCallingMessage( await window.textsecure.messaging.sendCallingMessage(
@ -292,7 +299,7 @@ export class CallingClass {
this.addCallHistoryForAutoEndedIncomingCall(conversation, reason); this.addCallHistoryForAutoEndedIncomingCall(conversation, reason);
} }
private attachToCall(conversation: ConversationType, call: Call): void { private attachToCall(conversation: ConversationModelType, call: Call): void {
const { uxActions } = this; const { uxActions } = this;
if (!uxActions) { if (!uxActions) {
return; return;
@ -340,7 +347,7 @@ export class CallingClass {
} }
private getRemoteUserIdFromConversation( private getRemoteUserIdFromConversation(
conversation: ConversationType conversation: ConversationModelType
): UserId | undefined { ): UserId | undefined {
const recipients = conversation.getRecipients(); const recipients = conversation.getRecipients();
if (recipients.length !== 1) { if (recipients.length !== 1) {
@ -366,8 +373,12 @@ export class CallingClass {
} }
private async getCallSettings( private async getCallSettings(
conversation: ConversationType conversation: ConversationModelType
): Promise<CallSettings> { ): Promise<CallSettings> {
if (!window.textsecure.messaging) {
throw new Error('getCallSettings: offline!');
}
const iceServerJson = await window.textsecure.messaging.server.getIceServers(); const iceServerJson = await window.textsecure.messaging.server.getIceServers();
const shouldRelayCalls = Boolean(await window.getAlwaysRelayCalls()); const shouldRelayCalls = Boolean(await window.getAlwaysRelayCalls());
@ -382,7 +393,7 @@ export class CallingClass {
} }
private getUxCallDetails( private getUxCallDetails(
conversation: ConversationType, conversation: ConversationModelType,
call: Call call: Call
): CallDetailsType { ): CallDetailsType {
return { return {
@ -398,7 +409,7 @@ export class CallingClass {
} }
private addCallHistoryForEndedCall( private addCallHistoryForEndedCall(
conversation: ConversationType, conversation: ConversationModelType,
call: Call, call: Call,
acceptedTime: number | undefined acceptedTime: number | undefined
) { ) {
@ -429,7 +440,7 @@ export class CallingClass {
} }
private addCallHistoryForFailedIncomingCall( private addCallHistoryForFailedIncomingCall(
conversation: ConversationType, conversation: ConversationModelType,
call: Call call: Call
) { ) {
const callHistoryDetails: CallHistoryDetailsType = { const callHistoryDetails: CallHistoryDetailsType = {
@ -444,7 +455,7 @@ export class CallingClass {
} }
private addCallHistoryForAutoEndedIncomingCall( private addCallHistoryForAutoEndedIncomingCall(
conversation: ConversationType, conversation: ConversationModelType,
_reason: CallEndedReason _reason: CallEndedReason
) { ) {
const callHistoryDetails: CallHistoryDetailsType = { const callHistoryDetails: CallHistoryDetailsType = {

View file

@ -19,12 +19,15 @@ import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto';
import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message'; import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message';
import { createBatcher } from '../util/batcher'; import { createBatcher } from '../util/batcher';
import {
ConversationModelCollectionType,
ConversationModelType,
MessageModelCollectionType,
MessageModelType,
} from '../model-types.d';
import { import {
AttachmentDownloadJobType, AttachmentDownloadJobType,
BackboneConversationCollectionType,
BackboneConversationModelType,
BackboneMessageCollectionType,
BackboneMessageModelType,
ClientInterface, ClientInterface,
ClientJobType, ClientJobType,
ConversationType, ConversationType,
@ -153,6 +156,7 @@ const dataInterface: ClientInterface = {
getOlderMessagesByConversation, getOlderMessagesByConversation,
getNewerMessagesByConversation, getNewerMessagesByConversation,
getMessageMetricsForConversation, getMessageMetricsForConversation,
migrateConversationMessages,
getUnprocessedCount, getUnprocessedCount,
getAllUnprocessed, getAllUnprocessed,
@ -719,7 +723,7 @@ async function saveConversations(array: Array<ConversationType>) {
async function getConversationById( async function getConversationById(
id: string, id: string,
{ Conversation }: { Conversation: BackboneConversationModelType } { Conversation }: { Conversation: typeof ConversationModelType }
) { ) {
const data = await channels.getConversationById(id); const data = await channels.getConversationById(id);
@ -749,7 +753,7 @@ async function updateConversations(array: Array<ConversationType>) {
async function removeConversation( async function removeConversation(
id: string, id: string,
{ Conversation }: { Conversation: BackboneConversationModelType } { Conversation }: { Conversation: typeof ConversationModelType }
) { ) {
const existing = await getConversationById(id, { Conversation }); const existing = await getConversationById(id, { Conversation });
@ -769,8 +773,8 @@ async function _removeConversations(ids: Array<string>) {
async function getAllConversations({ async function getAllConversations({
ConversationCollection, ConversationCollection,
}: { }: {
ConversationCollection: BackboneConversationCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
}) { }): Promise<ConversationModelCollectionType> {
const conversations = await channels.getAllConversations(); const conversations = await channels.getAllConversations();
const collection = new ConversationCollection(); const collection = new ConversationCollection();
@ -788,7 +792,7 @@ async function getAllConversationIds() {
async function getAllPrivateConversations({ async function getAllPrivateConversations({
ConversationCollection, ConversationCollection,
}: { }: {
ConversationCollection: BackboneConversationCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
}) { }) {
const conversations = await channels.getAllPrivateConversations(); const conversations = await channels.getAllPrivateConversations();
@ -803,7 +807,7 @@ async function getAllGroupsInvolvingId(
{ {
ConversationCollection, ConversationCollection,
}: { }: {
ConversationCollection: BackboneConversationCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
} }
) { ) {
const conversations = await channels.getAllGroupsInvolvingId(id); const conversations = await channels.getAllGroupsInvolvingId(id);
@ -861,7 +865,7 @@ async function saveMessage(
{ {
forceSave, forceSave,
Message, Message,
}: { forceSave?: boolean; Message: BackboneMessageModelType } }: { forceSave?: boolean; Message: typeof MessageModelType }
) { ) {
const id = await channels.saveMessage(_cleanData(data), { forceSave }); const id = await channels.saveMessage(_cleanData(data), { forceSave });
Message.updateTimers(); Message.updateTimers();
@ -878,7 +882,7 @@ async function saveMessages(
async function removeMessage( async function removeMessage(
id: string, id: string,
{ Message }: { Message: BackboneMessageModelType } { Message }: { Message: typeof MessageModelType }
) { ) {
const message = await getMessageById(id, { Message }); const message = await getMessageById(id, { Message });
@ -897,7 +901,7 @@ async function _removeMessages(ids: Array<string>) {
async function getMessageById( async function getMessageById(
id: string, id: string,
{ Message }: { Message: BackboneMessageModelType } { Message }: { Message: typeof MessageModelType }
) { ) {
const message = await channels.getMessageById(id); const message = await channels.getMessageById(id);
if (!message) { if (!message) {
@ -911,7 +915,7 @@ async function getMessageById(
async function _getAllMessages({ async function _getAllMessages({
MessageCollection, MessageCollection,
}: { }: {
MessageCollection: BackboneMessageCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) { }) {
const messages = await channels._getAllMessages(); const messages = await channels._getAllMessages();
@ -936,7 +940,7 @@ async function getMessageBySender(
sourceDevice: string; sourceDevice: string;
sent_at: number; sent_at: number;
}, },
{ Message }: { Message: BackboneMessageModelType } { Message }: { Message: typeof MessageModelType }
) { ) {
const messages = await channels.getMessageBySender({ const messages = await channels.getMessageBySender({
source, source,
@ -953,7 +957,9 @@ async function getMessageBySender(
async function getUnreadByConversation( async function getUnreadByConversation(
conversationId: string, conversationId: string,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType } {
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) { ) {
const messages = await channels.getUnreadByConversation(conversationId); const messages = await channels.getUnreadByConversation(conversationId);
@ -975,7 +981,7 @@ async function getOlderMessagesByConversation(
limit?: number; limit?: number;
receivedAt?: number; receivedAt?: number;
messageId?: string; messageId?: string;
MessageCollection: BackboneMessageCollectionType; MessageCollection: typeof MessageModelCollectionType;
} }
) { ) {
const messages = await channels.getOlderMessagesByConversation( const messages = await channels.getOlderMessagesByConversation(
@ -998,7 +1004,7 @@ async function getNewerMessagesByConversation(
}: { }: {
limit?: number; limit?: number;
receivedAt?: number; receivedAt?: number;
MessageCollection: BackboneMessageCollectionType; MessageCollection: typeof MessageModelCollectionType;
} }
) { ) {
const messages = await channels.getNewerMessagesByConversation( const messages = await channels.getNewerMessagesByConversation(
@ -1018,10 +1024,18 @@ async function getMessageMetricsForConversation(conversationId: string) {
return result; return result;
} }
async function migrateConversationMessages(
obsoleteId: string,
currentId: string
) {
await channels.migrateConversationMessages(obsoleteId, currentId);
}
async function removeAllMessagesInConversation( async function removeAllMessagesInConversation(
conversationId: string, conversationId: string,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType } {
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) { ) {
let messages; let messages;
do { do {
@ -1036,12 +1050,12 @@ async function removeAllMessagesInConversation(
return; return;
} }
const ids = messages.map((message: BackboneMessageModelType) => message.id); const ids = messages.map((message: MessageModelType) => message.id);
// Note: It's very important that these models are fully hydrated because // Note: It's very important that these models are fully hydrated because
// we need to delete all associated on-disk files along with the database delete. // we need to delete all associated on-disk files along with the database delete.
await Promise.all( await Promise.all(
messages.map((message: BackboneMessageModelType) => message.cleanup()) messages.map(async (message: MessageModelType) => message.cleanup())
); );
await channels.removeMessage(ids); await channels.removeMessage(ids);
@ -1050,7 +1064,9 @@ async function removeAllMessagesInConversation(
async function getMessagesBySentAt( async function getMessagesBySentAt(
sentAt: number, sentAt: number,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType } {
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) { ) {
const messages = await channels.getMessagesBySentAt(sentAt); const messages = await channels.getMessagesBySentAt(sentAt);
@ -1060,7 +1076,7 @@ async function getMessagesBySentAt(
async function getExpiredMessages({ async function getExpiredMessages({
MessageCollection, MessageCollection,
}: { }: {
MessageCollection: BackboneMessageCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) { }) {
const messages = await channels.getExpiredMessages(); const messages = await channels.getExpiredMessages();
@ -1070,7 +1086,7 @@ async function getExpiredMessages({
async function getOutgoingWithoutExpiresAt({ async function getOutgoingWithoutExpiresAt({
MessageCollection, MessageCollection,
}: { }: {
MessageCollection: BackboneMessageCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) { }) {
const messages = await channels.getOutgoingWithoutExpiresAt(); const messages = await channels.getOutgoingWithoutExpiresAt();
@ -1080,7 +1096,7 @@ async function getOutgoingWithoutExpiresAt({
async function getNextExpiringMessage({ async function getNextExpiringMessage({
Message, Message,
}: { }: {
Message: BackboneMessageModelType; Message: typeof MessageModelType;
}) { }) {
const message = await channels.getNextExpiringMessage(); const message = await channels.getNextExpiringMessage();
@ -1094,7 +1110,7 @@ async function getNextExpiringMessage({
async function getNextTapToViewMessageToAgeOut({ async function getNextTapToViewMessageToAgeOut({
Message, Message,
}: { }: {
Message: BackboneMessageModelType; Message: typeof MessageModelType;
}) { }) {
const message = await channels.getNextTapToViewMessageToAgeOut(); const message = await channels.getNextTapToViewMessageToAgeOut();
if (!message) { if (!message) {
@ -1106,7 +1122,7 @@ async function getNextTapToViewMessageToAgeOut({
async function getTapToViewMessagesNeedingErase({ async function getTapToViewMessagesNeedingErase({
MessageCollection, MessageCollection,
}: { }: {
MessageCollection: BackboneMessageCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) { }) {
const messages = await channels.getTapToViewMessagesNeedingErase(); const messages = await channels.getTapToViewMessagesNeedingErase();

View file

@ -17,10 +17,12 @@ export type StickerPackType = any;
export type StickerType = any; export type StickerType = any;
export type UnprocessedType = any; export type UnprocessedType = any;
export type BackboneConversationModelType = any; import {
export type BackboneConversationCollectionType = any; ConversationModelCollectionType,
export type BackboneMessageModelType = any; ConversationModelType,
export type BackboneMessageCollectionType = any; MessageModelCollectionType,
MessageModelType,
} from '../model-types.d';
export interface DataInterface { export interface DataInterface {
close: () => Promise<void>; close: () => Promise<void>;
@ -94,6 +96,10 @@ export interface DataInterface {
getMessageMetricsForConversation: ( getMessageMetricsForConversation: (
conversationId: string conversationId: string
) => Promise<ConverationMetricsType>; ) => Promise<ConverationMetricsType>;
migrateConversationMessages: (
obsoleteId: string,
currentId: string
) => Promise<void>;
getUnprocessedCount: () => Promise<number>; getUnprocessedCount: () => Promise<number>;
getAllUnprocessed: () => Promise<Array<UnprocessedType>>; getAllUnprocessed: () => Promise<Array<UnprocessedType>>;
@ -242,33 +248,33 @@ export type ClientInterface = DataInterface & {
getAllConversations: ({ getAllConversations: ({
ConversationCollection, ConversationCollection,
}: { }: {
ConversationCollection: BackboneConversationCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
}) => Promise<Array<ConversationType>>; }) => Promise<ConversationModelCollectionType>;
getAllGroupsInvolvingId: ( getAllGroupsInvolvingId: (
id: string, id: string,
{ {
ConversationCollection, ConversationCollection,
}: { }: {
ConversationCollection: BackboneConversationCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
} }
) => Promise<Array<ConversationType>>; ) => Promise<ConversationModelCollectionType>;
getAllPrivateConversations: ({ getAllPrivateConversations: ({
ConversationCollection, ConversationCollection,
}: { }: {
ConversationCollection: BackboneConversationCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
}) => Promise<Array<ConversationType>>; }) => Promise<ConversationModelCollectionType>;
getConversationById: ( getConversationById: (
id: string, id: string,
{ Conversation }: { Conversation: BackboneConversationModelType } { Conversation }: { Conversation: typeof ConversationModelType }
) => Promise<ConversationType>; ) => Promise<ConversationModelType>;
getExpiredMessages: ({ getExpiredMessages: ({
MessageCollection, MessageCollection,
}: { }: {
MessageCollection: BackboneMessageCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) => Promise<Array<MessageType>>; }) => Promise<MessageModelCollectionType>;
getMessageById: ( getMessageById: (
id: string, id: string,
{ Message }: { Message: BackboneMessageModelType } { Message }: { Message: typeof MessageModelType }
) => Promise<MessageType | undefined>; ) => Promise<MessageType | undefined>;
getMessageBySender: ( getMessageBySender: (
options: { options: {
@ -277,63 +283,67 @@ export type ClientInterface = DataInterface & {
sourceDevice: string; sourceDevice: string;
sent_at: number; sent_at: number;
}, },
{ Message }: { Message: BackboneMessageModelType } { Message }: { Message: typeof MessageModelType }
) => Promise<Array<MessageType>>; ) => Promise<MessageModelType | null>;
getMessagesBySentAt: ( getMessagesBySentAt: (
sentAt: number, sentAt: number,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType } {
) => Promise<Array<MessageType>>; MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) => Promise<MessageModelCollectionType>;
getOlderMessagesByConversation: ( getOlderMessagesByConversation: (
conversationId: string, conversationId: string,
options: { options: {
limit?: number; limit?: number;
receivedAt?: number; receivedAt?: number;
MessageCollection: BackboneMessageCollectionType; MessageCollection: typeof MessageModelCollectionType;
} }
) => Promise<Array<MessageTypeUnhydrated>>; ) => Promise<MessageModelCollectionType>;
getNewerMessagesByConversation: ( getNewerMessagesByConversation: (
conversationId: string, conversationId: string,
options: { options: {
limit?: number; limit?: number;
receivedAt?: number; receivedAt?: number;
MessageCollection: BackboneMessageCollectionType; MessageCollection: typeof MessageModelCollectionType;
} }
) => Promise<Array<MessageTypeUnhydrated>>; ) => Promise<MessageModelCollectionType>;
getNextExpiringMessage: ({ getNextExpiringMessage: ({
Message, Message,
}: { }: {
Message: BackboneMessageModelType; Message: typeof MessageModelType;
}) => Promise<MessageType>; }) => Promise<MessageModelType | null>;
getNextTapToViewMessageToAgeOut: ({ getNextTapToViewMessageToAgeOut: ({
Message, Message,
}: { }: {
Message: BackboneMessageModelType; Message: typeof MessageModelType;
}) => Promise<MessageType>; }) => Promise<MessageModelType | null>;
getOutgoingWithoutExpiresAt: ({ getOutgoingWithoutExpiresAt: ({
MessageCollection, MessageCollection,
}: { }: {
MessageCollection: BackboneMessageCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) => Promise<Array<MessageType>>; }) => Promise<MessageModelCollectionType>;
getTapToViewMessagesNeedingErase: ({ getTapToViewMessagesNeedingErase: ({
MessageCollection, MessageCollection,
}: { }: {
MessageCollection: BackboneMessageCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) => Promise<Array<MessageType>>; }) => Promise<MessageModelCollectionType>;
getUnreadByConversation: ( getUnreadByConversation: (
conversationId: string, conversationId: string,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType } {
) => Promise<Array<MessageType>>; MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) => Promise<MessageModelCollectionType>;
removeConversation: ( removeConversation: (
id: string, id: string,
{ Conversation }: { Conversation: BackboneConversationModelType } { Conversation }: { Conversation: typeof ConversationModelType }
) => Promise<void>; ) => Promise<void>;
removeMessage: ( removeMessage: (
id: string, id: string,
{ Message }: { Message: BackboneMessageModelType } { Message }: { Message: typeof MessageModelType }
) => Promise<void>; ) => Promise<void>;
saveMessage: ( saveMessage: (
data: MessageType, data: MessageType,
options: { forceSave?: boolean; Message: BackboneMessageModelType } options: { forceSave?: boolean; Message: typeof MessageModelType }
) => Promise<number>; ) => Promise<number>;
updateConversation: (data: ConversationType) => void; updateConversation: (data: ConversationType) => void;
@ -342,15 +352,17 @@ export type ClientInterface = DataInterface & {
_getAllMessages: ({ _getAllMessages: ({
MessageCollection, MessageCollection,
}: { }: {
MessageCollection: BackboneMessageCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) => Promise<Array<MessageType>>; }) => Promise<MessageModelCollectionType>;
// Client-side only // Client-side only
shutdown: () => Promise<void>; shutdown: () => Promise<void>;
removeAllMessagesInConversation: ( removeAllMessagesInConversation: (
conversationId: string, conversationId: string,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType } {
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) => Promise<void>; ) => Promise<void>;
removeOtherData: () => Promise<void>; removeOtherData: () => Promise<void>;
cleanupOrphanedAttachments: () => Promise<void>; cleanupOrphanedAttachments: () => Promise<void>;

View file

@ -131,6 +131,7 @@ const dataInterface: ServerInterface = {
getOlderMessagesByConversation, getOlderMessagesByConversation,
getNewerMessagesByConversation, getNewerMessagesByConversation,
getMessageMetricsForConversation, getMessageMetricsForConversation,
migrateConversationMessages,
getUnprocessedCount, getUnprocessedCount,
getAllUnprocessed, getAllUnprocessed,
@ -2797,6 +2798,25 @@ async function getMessageMetricsForConversation(conversationId: string) {
} }
getMessageMetricsForConversation.needsSerial = true; getMessageMetricsForConversation.needsSerial = true;
async function migrateConversationMessages(
obsoleteId: string,
currentId: string
) {
const db = getInstance();
await db.run(
`UPDATE messages SET
conversationId = $currentId,
json = json_set(json, '$.conversationId', $currentId)
WHERE conversationId = $obsoleteId;`,
{
$obsoleteId: obsoleteId,
$currentId: currentId,
}
);
}
migrateConversationMessages.needsSerial = true;
async function getMessagesBySentAt(sentAt: number) { async function getMessagesBySentAt(sentAt: number) {
const db = getInstance(); const db = getInstance();
const rows = await db.all( const rows = await db.all(

29
ts/textsecure.d.ts vendored
View file

@ -10,6 +10,8 @@ import EventTarget from './textsecure/EventTarget';
import { ByteBufferClass } from './window.d'; import { ByteBufferClass } from './window.d';
import { SendOptionsType } from './textsecure/SendMessage'; import { SendOptionsType } from './textsecure/SendMessage';
import { WebAPIType } from './textsecure/WebAPI'; import { WebAPIType } from './textsecure/WebAPI';
import utils from './textsecure/Helpers';
import SendMessage from './textsecure/SendMessage';
type AttachmentType = any; type AttachmentType = any;
@ -79,31 +81,9 @@ export type TextSecureType = {
attachment: AttachmentPointerClass attachment: AttachmentPointerClass
) => Promise<DownloadAttachmentType>; ) => Promise<DownloadAttachmentType>;
}; };
messaging: { messaging?: SendMessage;
getStorageCredentials: () => Promise<StorageServiceCredentials>;
getStorageManifest: (
options: StorageServiceCallOptionsType
) => Promise<ArrayBuffer>;
getStorageRecords: (
data: ArrayBuffer,
options: StorageServiceCallOptionsType
) => Promise<ArrayBuffer>;
sendStickerPackSync: (
operations: Array<{
packId: string;
packKey: string;
installed: boolean;
}>,
options: Object
) => Promise<void>;
sendCallingMessage: (
recipientId: string,
callingMessage: CallingMessageClass,
sendOptions: SendOptionsType
) => Promise<void>;
server: WebAPIType;
};
protobuf: ProtobufCollectionType; protobuf: ProtobufCollectionType;
utils: typeof utils;
EventTarget: typeof EventTarget; EventTarget: typeof EventTarget;
MessageReceiver: typeof MessageReceiver; MessageReceiver: typeof MessageReceiver;
@ -150,6 +130,7 @@ export type StorageProtocolType = StorageType & {
verifiedStatus: number, verifiedStatus: number,
publicKey: ArrayBuffer publicKey: ArrayBuffer
) => Promise<boolean>; ) => Promise<boolean>;
removeIdentityKey: (identifier: string) => Promise<void>;
saveIdentityWithAttributes: ( saveIdentityWithAttributes: (
number: string, number: string,
options: IdentityKeyRecord options: IdentityKeyRecord

View file

@ -696,6 +696,7 @@ export default class AccountManager extends EventTarget {
const conversationId = window.ConversationController.ensureContactIds({ const conversationId = window.ConversationController.ensureContactIds({
e164: number, e164: number,
uuid, uuid,
highTrust: true,
}); });
if (!conversationId) { if (!conversationId) {
throw new Error('registrationDone: no conversationId!'); throw new Error('registrationDone: no conversationId!');

View file

@ -1058,6 +1058,7 @@ class MessageReceiverInner extends EventTarget {
) { ) {
const { const {
destination, destination,
destinationUuid,
timestamp, timestamp,
message: msg, message: msg,
expirationStartTimestamp, expirationStartTimestamp,
@ -1083,12 +1084,13 @@ class MessageReceiverInner extends EventTarget {
msg.flags && msg.flags &&
msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION
) { ) {
if (!destination) { const identifier = destination || destinationUuid;
if (!identifier) {
throw new Error( throw new Error(
'MessageReceiver.handleSentMessage: Cannot end session with falsey destination' 'MessageReceiver.handleSentMessage: Cannot end session with falsey destination'
); );
} }
p = this.handleEndSession(destination); p = this.handleEndSession(identifier);
} }
return p.then(async () => return p.then(async () =>
this.processDecrypted(envelope, msg).then(message => { this.processDecrypted(envelope, msg).then(message => {
@ -1120,6 +1122,7 @@ class MessageReceiverInner extends EventTarget {
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = { ev.data = {
destination, destination,
destinationUuid,
timestamp: timestamp.toNumber(), timestamp: timestamp.toNumber(),
serverTimestamp: envelope.serverTimestamp, serverTimestamp: envelope.serverTimestamp,
device: envelope.sourceDevice, device: envelope.sourceDevice,
@ -1303,7 +1306,8 @@ class MessageReceiverInner extends EventTarget {
ev.timestamp = envelope.timestamp.toNumber(); ev.timestamp = envelope.timestamp.toNumber();
ev.read = { ev.read = {
timestamp: receiptMessage.timestamp[i].toNumber(), timestamp: receiptMessage.timestamp[i].toNumber(),
reader: envelope.source || envelope.sourceUuid, source: envelope.source,
sourceUuid: envelope.sourceUuid,
}; };
results.push(this.dispatchAndWait(ev)); results.push(this.dispatchAndWait(ev));
} }

View file

@ -925,7 +925,7 @@ export default class MessageSender {
async sendCallingMessage( async sendCallingMessage(
recipientId: string, recipientId: string,
callingMessage: CallingMessageClass, callingMessage: CallingMessageClass,
sendOptions: SendOptionsType sendOptions?: SendOptionsType
) { ) {
const recipients = [recipientId]; const recipients = [recipientId];
const finalTimestamp = Date.now(); const finalTimestamp = Date.now();
@ -1001,7 +1001,11 @@ export default class MessageSender {
); );
} }
async syncReadMessages( async syncReadMessages(
reads: Array<{ sender: string; timestamp: number }>, reads: Array<{
senderUuid?: string;
senderE164?: string;
timestamp: number;
}>,
options?: SendOptionsType options?: SendOptionsType
) { ) {
const myNumber = window.textsecure.storage.user.getNumber(); const myNumber = window.textsecure.storage.user.getNumber();
@ -1013,7 +1017,8 @@ export default class MessageSender {
for (let i = 0; i < reads.length; i += 1) { for (let i = 0; i < reads.length; i += 1) {
const read = new window.textsecure.protobuf.SyncMessage.Read(); const read = new window.textsecure.protobuf.SyncMessage.Read();
read.timestamp = reads[i].timestamp; read.timestamp = reads[i].timestamp;
read.sender = reads[i].sender; read.sender = reads[i].senderE164;
read.senderUuid = reads[i].senderUuid;
syncMessage.read.push(read); syncMessage.read.push(read);
} }
@ -1352,20 +1357,20 @@ export default class MessageSender {
proto.flags = window.textsecure.protobuf.DataMessage.Flags.END_SESSION; proto.flags = window.textsecure.protobuf.DataMessage.Flags.END_SESSION;
proto.timestamp = timestamp; proto.timestamp = timestamp;
const identifier = e164 || uuid; const identifier = uuid || e164;
const logError = (prefix: string) => (error: Error) => { const logError = (prefix: string) => (error: Error) => {
window.log.error(prefix, error && error.stack ? error.stack : error); window.log.error(prefix, error && error.stack ? error.stack : error);
throw error; throw error;
}; };
const deleteAllSessions = async (targetNumber: string) => const deleteAllSessions = async (targetIdentifier: string) =>
window.textsecure.storage.protocol window.textsecure.storage.protocol
.getDeviceIds(targetNumber) .getDeviceIds(targetIdentifier)
.then(async deviceIds => .then(async deviceIds =>
Promise.all( Promise.all(
deviceIds.map(async deviceId => { deviceIds.map(async deviceId => {
const address = new window.libsignal.SignalProtocolAddress( const address = new window.libsignal.SignalProtocolAddress(
targetNumber, targetIdentifier,
deviceId deviceId
); );
window.log.info('deleting sessions for', address.toString()); window.log.info('deleting sessions for', address.toString());
@ -1401,7 +1406,7 @@ export default class MessageSender {
const myNumber = window.textsecure.storage.user.getNumber(); const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
// We already sent the reset session to our other devices in the code above! // We already sent the reset session to our other devices in the code above!
if (e164 === myNumber || uuid === myUuid) { if ((e164 && e164 === myNumber) || (uuid && uuid === myUuid)) {
return sendToContactPromise; return sendToContactPromise;
} }

View file

@ -6,6 +6,7 @@ import { Agent } from 'https';
import is from '@sindresorhus/is'; import is from '@sindresorhus/is';
import { redactPackId } from '../../js/modules/stickers'; import { redactPackId } from '../../js/modules/stickers';
import { getRandomValue } from '../Crypto'; import { getRandomValue } from '../Crypto';
import MessageSender from './SendMessage';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid'; import { v4 as getGuid } from 'uuid';
@ -13,7 +14,6 @@ import { v4 as getGuid } from 'uuid';
import { import {
StorageServiceCallOptionsType, StorageServiceCallOptionsType,
StorageServiceCredentials, StorageServiceCredentials,
TextSecureType,
} from '../textsecure.d'; } from '../textsecure.d';
// tslint:disable no-bitwise // tslint:disable no-bitwise
@ -589,9 +589,9 @@ export type WebAPIType = {
getSenderCertificate: (withUuid?: boolean) => Promise<any>; getSenderCertificate: (withUuid?: boolean) => Promise<any>;
getSticker: (packId: string, stickerId: string) => Promise<any>; getSticker: (packId: string, stickerId: string) => Promise<any>;
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>; getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
getStorageCredentials: TextSecureType['messaging']['getStorageCredentials']; getStorageCredentials: MessageSender['getStorageCredentials'];
getStorageManifest: TextSecureType['messaging']['getStorageManifest']; getStorageManifest: MessageSender['getStorageManifest'];
getStorageRecords: TextSecureType['messaging']['getStorageRecords']; getStorageRecords: MessageSender['getStorageRecords'];
makeProxiedRequest: ( makeProxiedRequest: (
targetUrl: string, targetUrl: string,
options?: ProxiedRequestOptionsType options?: ProxiedRequestOptionsType

View file

@ -160,22 +160,6 @@
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{
"rule": "jQuery-load(",
"path": "js/conversation_controller.js",
"line": " async load() {",
"lineNumber": 306,
"reasonCategory": "falseMatch",
"updated": "2020-06-19T18:29:40.067Z"
},
{
"rule": "jQuery-load(",
"path": "js/conversation_controller.js",
"line": " this._initialPromise = load();",
"lineNumber": 348,
"reasonCategory": "falseMatch",
"updated": "2020-06-19T18:29:40.067Z"
},
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/debug_log_start.js", "path": "js/debug_log_start.js",
@ -223,7 +207,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "js/models/conversations.js", "path": "js/models/conversations.js",
"line": " await wrap(", "line": " await wrap(",
"lineNumber": 652, "lineNumber": 663,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-06-09T20:26:46.515Z" "updated": "2020-06-09T20:26:46.515Z"
}, },
@ -504,7 +488,7 @@
"rule": "jQuery-prependTo(", "rule": "jQuery-prependTo(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
"line": " this.appLoadingScreen.$el.prependTo(this.el);", "line": " this.appLoadingScreen.$el.prependTo(this.el);",
"lineNumber": 90, "lineNumber": 97,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z", "updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Known DOM elements" "reasonDetail": "Known DOM elements"
@ -513,7 +497,7 @@
"rule": "jQuery-appendTo(", "rule": "jQuery-appendTo(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
"line": " toast.$el.appendTo(this.$el);", "line": " toast.$el.appendTo(this.$el);",
"lineNumber": 99, "lineNumber": 106,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z", "updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Known DOM elements" "reasonDetail": "Known DOM elements"
@ -522,7 +506,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
"line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);", "line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);",
"lineNumber": 121, "lineNumber": 128,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z", "updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "<optional>" "reasonDetail": "<optional>"
@ -531,7 +515,7 @@
"rule": "jQuery-append(", "rule": "jQuery-append(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
"line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);", "line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);",
"lineNumber": 121, "lineNumber": 128,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z", "updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "<optional>" "reasonDetail": "<optional>"
@ -540,7 +524,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);", "line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
"lineNumber": 132, "lineNumber": 139,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z", "updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Known DOM elements" "reasonDetail": "Known DOM elements"
@ -549,7 +533,7 @@
"rule": "jQuery-append(", "rule": "jQuery-append(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);", "line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
"lineNumber": 132, "lineNumber": 139,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z", "updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Known DOM elements" "reasonDetail": "Known DOM elements"
@ -558,7 +542,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
"line": " if (e && this.$(e.target).closest('.placeholder').length) {", "line": " if (e && this.$(e.target).closest('.placeholder').length) {",
"lineNumber": 183, "lineNumber": 190,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z", "updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Known DOM elements" "reasonDetail": "Known DOM elements"
@ -567,7 +551,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
"line": " this.$('#header, .gutter').addClass('inactive');", "line": " this.$('#header, .gutter').addClass('inactive');",
"lineNumber": 187, "lineNumber": 194,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z", "updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Hardcoded selector" "reasonDetail": "Hardcoded selector"
@ -576,7 +560,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
"line": " this.$('.conversation-stack').addClass('inactive');", "line": " this.$('.conversation-stack').addClass('inactive');",
"lineNumber": 191, "lineNumber": 198,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z", "updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Hardcoded selector" "reasonDetail": "Hardcoded selector"
@ -585,7 +569,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
"line": " this.$('.conversation:first .menu').trigger('close');", "line": " this.$('.conversation:first .menu').trigger('close');",
"lineNumber": 193, "lineNumber": 200,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z", "updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Hardcoded selector" "reasonDetail": "Hardcoded selector"
@ -594,7 +578,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {", "line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
"lineNumber": 213, "lineNumber": 220,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-29T18:29:18.234Z", "updated": "2020-05-29T18:29:18.234Z",
"reasonDetail": "Known DOM elements" "reasonDetail": "Known DOM elements"
@ -603,7 +587,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
"line": " this.$('.conversation:first .recorder').trigger('close');", "line": " this.$('.conversation:first .recorder').trigger('close');",
"lineNumber": 216, "lineNumber": 223,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-29T18:29:18.234Z", "updated": "2020-05-29T18:29:18.234Z",
"reasonDetail": "Hardcoded selector" "reasonDetail": "Hardcoded selector"
@ -11875,4 +11859,4 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z" "updated": "2020-04-05T23:45:16.746Z"
} }
] ]

View file

@ -60,6 +60,8 @@ const excludedFiles = [
'^ts/Crypto.ts', '^ts/Crypto.ts',
'^ts/textsecure/MessageReceiver.js', '^ts/textsecure/MessageReceiver.js',
'^ts/textsecure/MessageReceiver.ts', '^ts/textsecure/MessageReceiver.ts',
'^ts/ConversationController.js',
'^ts/ConversationController.ts',
// Generated files // Generated files
'^js/components.js', '^js/components.js',

View file

@ -17,7 +17,7 @@ import {
ManifestRecordClass, ManifestRecordClass,
StorageItemClass, StorageItemClass,
} from '../textsecure.d'; } from '../textsecure.d';
import { ConversationType } from '../window.d'; import { ConversationModelType } from '../model-types.d';
function fromRecordVerified(verified: number): number { function fromRecordVerified(verified: number): number {
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus; const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
@ -35,6 +35,11 @@ function fromRecordVerified(verified: number): number {
async function fetchManifest(manifestVersion: string) { async function fetchManifest(manifestVersion: string) {
window.log.info('storageService.fetchManifest'); window.log.info('storageService.fetchManifest');
if (!window.textsecure.messaging) {
throw new Error('fetchManifest: We are offline!');
}
try { try {
const credentials = await window.textsecure.messaging.getStorageCredentials(); const credentials = await window.textsecure.messaging.getStorageCredentials();
window.storage.put('storageCredentials', credentials); window.storage.put('storageCredentials', credentials);
@ -286,6 +291,10 @@ async function processManifest(
const storageKeyBase64 = window.storage.get('storageKey'); const storageKeyBase64 = window.storage.get('storageKey');
const storageKey = base64ToArrayBuffer(storageKeyBase64); const storageKey = base64ToArrayBuffer(storageKeyBase64);
if (!window.textsecure.messaging) {
throw new Error('processManifest: We are offline!');
}
const remoteKeysTypeMap = new Map(); const remoteKeysTypeMap = new Map();
manifest.keys.forEach(key => { manifest.keys.forEach(key => {
remoteKeysTypeMap.set( remoteKeysTypeMap.set(
@ -296,7 +305,7 @@ async function processManifest(
const localKeys = window const localKeys = window
.getConversations() .getConversations()
.map((conversation: ConversationType) => conversation.get('storageID')) .map((conversation: ConversationModelType) => conversation.get('storageID'))
.filter(Boolean); .filter(Boolean);
window.log.info( window.log.info(
`storageService.processManifest localKeys.length ${localKeys.length}` `storageService.processManifest localKeys.length ${localKeys.length}`

98
ts/window.d.ts vendored
View file

@ -1,6 +1,14 @@
// Captures the globals put in place by preload.js, background.js and others // Captures the globals put in place by preload.js, background.js and others
import * as Backbone from 'backbone';
import * as Underscore from 'underscore';
import { Ref } from 'react'; import { Ref } from 'react';
import {
ConversationModelCollectionType,
ConversationModelType,
MessageModelCollectionType,
MessageModelType,
} from './model-types.d';
import { import {
LibSignalType, LibSignalType,
SignalProtocolAddressClass, SignalProtocolAddressClass,
@ -11,6 +19,7 @@ import { WebAPIConnectType } from './textsecure/WebAPI';
import { CallingClass, CallHistoryDetailsType } from './services/calling'; import { CallingClass, CallHistoryDetailsType } from './services/calling';
import * as Crypto from './Crypto'; import * as Crypto from './Crypto';
import { ColorType, LocalizerType } from './types/Util'; import { ColorType, LocalizerType } from './types/Util';
import { ConversationController } from './ConversationController';
import { SendOptionsType } from './textsecure/SendMessage'; import { SendOptionsType } from './textsecure/SendMessage';
import Data from './sql/Client'; import Data from './sql/Client';
@ -19,18 +28,22 @@ type TaskResultType = any;
declare global { declare global {
interface Window { interface Window {
dcodeIO: DCodeIOType; dcodeIO: DCodeIOType;
getConversations: () => ConversationControllerType;
getExpiration: () => string;
getEnvironment: () => string;
getSocketStatus: () => number;
getAlwaysRelayCalls: () => Promise<boolean>; getAlwaysRelayCalls: () => Promise<boolean>;
getIncomingCallNotification: () => Promise<boolean>;
getCallRingtoneNotification: () => Promise<boolean>; getCallRingtoneNotification: () => Promise<boolean>;
getCallSystemNotification: () => Promise<boolean>; getCallSystemNotification: () => Promise<boolean>;
getMediaPermissions: () => Promise<boolean>; getConversations: () => ConversationModelCollectionType;
getEnvironment: () => string;
getExpiration: () => string;
getGuid: () => string;
getInboxCollection: () => ConversationModelCollectionType;
getIncomingCallNotification: () => Promise<boolean>;
getMediaCameraPermissions: () => Promise<boolean>; getMediaCameraPermissions: () => Promise<boolean>;
getMediaPermissions: () => Promise<boolean>;
getSocketStatus: () => number;
getTitle: () => string;
showCallingPermissionsPopup: (forCamera: boolean) => Promise<void>; showCallingPermissionsPopup: (forCamera: boolean) => Promise<void>;
i18n: LocalizerType; i18n: LocalizerType;
isValidGuid: (maybeGuid: string) => boolean;
libphonenumber: { libphonenumber: {
util: { util: {
getRegionCodeForNumber: (number: string) => string; getRegionCodeForNumber: (number: string) => string;
@ -46,6 +59,7 @@ declare global {
platform: string; platform: string;
restart: () => void; restart: () => void;
showWindow: () => void; showWindow: () => void;
setBadgeCount: (count: number) => void;
storage: { storage: {
put: (key: string, value: any) => void; put: (key: string, value: any) => void;
remove: (key: string) => void; remove: (key: string) => void;
@ -55,7 +69,9 @@ declare global {
removeBlockedNumber: (number: string) => void; removeBlockedNumber: (number: string) => void;
}; };
textsecure: TextSecureType; textsecure: TextSecureType;
updateTrayIcon: (count: number) => void;
Backbone: typeof Backbone;
Signal: { Signal: {
Crypto: typeof Crypto; Crypto: typeof Crypto;
Data: typeof Data; Data: typeof Data;
@ -69,7 +85,7 @@ declare global {
calling: CallingClass; calling: CallingClass;
}; };
}; };
ConversationController: ConversationControllerType; ConversationController: ConversationController;
WebAPI: WebAPIConnectType; WebAPI: WebAPIConnectType;
Whisper: WhisperType; Whisper: WhisperType;
@ -82,68 +98,6 @@ declare global {
} }
} }
export type ConversationAttributes = {
e164?: string | null;
isArchived?: boolean;
profileFamilyName?: string | null;
profileKey?: string | null;
profileName?: string | null;
profileSharing?: boolean;
name?: string;
storageID?: string;
uuid?: string | null;
verified?: number;
};
export type ConversationType = {
attributes: ConversationAttributes;
fromRecordVerified: (
verified: ContactRecordIdentityState
) => ContactRecordIdentityState;
set: (props: Partial<ConversationAttributes>) => void;
updateE164: (e164?: string) => void;
updateUuid: (uuid?: string) => void;
id: string;
get: (key: string) => any;
getAvatarPath(): string | undefined;
getColor(): ColorType | undefined;
getName(): string | undefined;
getNumber(): string;
getProfiles(): Promise<Array<Promise<void>>>;
getProfileName(): string | undefined;
getRecipients: () => Array<string>;
getSendOptions(): SendOptionsType;
getTitle(): string;
isVerified(): boolean;
safeGetVerified(): Promise<number>;
getIsAddedByContact(): boolean;
addCallHistory(details: CallHistoryDetailsType): void;
toggleVerified(): Promise<TaskResultType>;
};
export type ConversationControllerType = {
getOrCreateAndWait: (
identifier: string,
type: 'private' | 'group'
) => Promise<ConversationType>;
getOrCreate: (
identifier: string,
type: 'private' | 'group'
) => ConversationType;
getConversationId: (identifier: string) => string | null;
ensureContactIds: (o: { e164?: string; uuid?: string }) => string;
getOurConversationId: () => string | null;
prepareForSend: (
id: string,
options: Object
) => {
wrap: (promise: Promise<any>) => Promise<void>;
sendOptions: Object;
};
get: (identifier: string) => null | ConversationType;
map: (mapFn: (conversation: ConversationType) => any) => any;
};
export type DCodeIOType = { export type DCodeIOType = {
ByteBuffer: typeof ByteBufferClass; ByteBuffer: typeof ByteBufferClass;
Long: { Long: {
@ -212,7 +166,7 @@ export type LoggerType = (...args: Array<any>) => void;
export type WhisperType = { export type WhisperType = {
events: { events: {
trigger: (name: string, param1: any, param2: any) => void; trigger: (name: string, param1: any, param2?: any) => void;
}; };
Database: { Database: {
open: () => Promise<IDBDatabase>; open: () => Promise<IDBDatabase>;
@ -222,4 +176,8 @@ export type WhisperType = {
reject: Function reject: Function
) => void; ) => void;
}; };
ConversationCollection: typeof ConversationModelCollectionType;
Conversation: typeof ConversationModelType;
MessageCollection: typeof MessageModelCollectionType;
Message: typeof MessageModelType;
}; };

View file

@ -12,6 +12,9 @@
// Preferred by Prettier: // Preferred by Prettier:
"arrow-parens": [true, "ban-single-arg-parens"], "arrow-parens": [true, "ban-single-arg-parens"],
// Breaks when we use .extend() to create a Backbone subclass
"no-invalid-this": false,
"import-spacing": false, "import-spacing": false,
"indent": [true, "spaces", 2], "indent": [true, "spaces", 2],
"interface-name": [true, "never-prefix"], "interface-name": [true, "never-prefix"],

View file

@ -2232,20 +2232,13 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/jquery@*": "@types/jquery@*", "@types/jquery@3.5.0":
version "3.5.0" version "3.5.0"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.0.tgz#ccb7dfd317d02d4227dd3803c75297d0c10dad68" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.0.tgz#ccb7dfd317d02d4227dd3803c75297d0c10dad68"
integrity sha512-C7qQUjpMWDUNYQRTXsP5nbYYwCwwgy84yPgoTT7fPN69NH92wLeCtFaMsWeolJD1AF/6uQw3pYt62rzv83sMmw== integrity sha512-C7qQUjpMWDUNYQRTXsP5nbYYwCwwgy84yPgoTT7fPN69NH92wLeCtFaMsWeolJD1AF/6uQw3pYt62rzv83sMmw==
dependencies: dependencies:
"@types/sizzle" "*" "@types/sizzle" "*"
"@types/jquery@3.3.29":
version "3.3.29"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.29.tgz#680a2219ce3c9250483722fccf5570d1e2d08abd"
integrity sha512-FhJvBninYD36v3k6c+bVk1DSZwh7B5Dpb/Pyk3HKVsiohn0nhbefZZ+3JXbWQhFyt0MxSl2jRDdGQPHeOHFXrQ==
dependencies:
"@types/sizzle" "*"
"@types/js-yaml@3.12.0": "@types/js-yaml@3.12.0":
version "3.12.0" version "3.12.0"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.0.tgz#3494ce97358e2675e24e97a747ec23478eeaf8b6" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.0.tgz#3494ce97358e2675e24e97a747ec23478eeaf8b6"
@ -2523,6 +2516,11 @@
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.14.tgz#a2a831c72a12deddaef26028d16a5aa48aadbee0" resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.14.tgz#a2a831c72a12deddaef26028d16a5aa48aadbee0"
integrity sha512-VE20ZYf38nmOU1lU0wpQBWcGPlskfKK8uU8AN1UIz5PjxT2YM7HTF0iUA85iGJnbQ3tZweqIfQqmLgLMtP27YQ== integrity sha512-VE20ZYf38nmOU1lU0wpQBWcGPlskfKK8uU8AN1UIz5PjxT2YM7HTF0iUA85iGJnbQ3tZweqIfQqmLgLMtP27YQ==
"@types/underscore@1.10.3":
version "1.10.3"
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.3.tgz#927ff2b2e516444587fc80fb7845bb5c1806aad2"
integrity sha512-WgNbx0H2QO4ccIk2R1aWkteETuPxSa9OYKXoYujBgc0R4u+d2PWsb9MPpP77H+xqwbCXT+wuEBQ/6fl6s4C0OA==
"@types/uuid@3.4.4": "@types/uuid@3.4.4":
version "3.4.4" version "3.4.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5"