Passive UUID support

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
Ken Powers 2020-03-05 13:14:58 -08:00 committed by Scott Nonnenberg
parent f64ca0ed21
commit a90246cbe5
49 changed files with 2226 additions and 776 deletions

View file

@ -5,6 +5,7 @@
'use strict';
const BLOCKED_NUMBERS_ID = 'blocked';
const BLOCKED_UUIDS_ID = 'blocked-uuids';
const BLOCKED_GROUPS_ID = 'blocked-groups';
storage.isBlocked = number => {
@ -31,6 +32,30 @@
storage.put(BLOCKED_NUMBERS_ID, _.without(numbers, number));
};
storage.isUuidBlocked = uuid => {
const uuids = storage.get(BLOCKED_UUIDS_ID, []);
return _.include(uuids, uuid);
};
storage.addBlockedUuid = uuid => {
const uuids = storage.get(BLOCKED_UUIDS_ID, []);
if (_.include(uuids, uuid)) {
return;
}
window.log.info('adding', uuid, 'to blocked list');
storage.put(BLOCKED_UUIDS_ID, uuids.concat(uuid));
};
storage.removeBlockedUuid = uuid => {
const numbers = storage.get(BLOCKED_UUIDS_ID, []);
if (!_.include(numbers, uuid)) {
return;
}
window.log.info('removing', uuid, 'from blocked list');
storage.put(BLOCKED_NUMBERS_ID, _.without(numbers, uuid));
};
storage.isGroupBlocked = groupId => {
const groupIds = storage.get(BLOCKED_GROUPS_ID, []);

View file

@ -27,7 +27,7 @@
};
const { Util } = window.Signal;
const { Conversation, Contact, Message, PhoneNumber } = window.Signal.Types;
const { Conversation, Contact, Message } = window.Signal.Types;
const {
deleteAttachmentData,
doesAttachmentExist,
@ -85,8 +85,13 @@
return collection;
},
initialize() {
initialize(attributes) {
if (window.isValidE164(attributes.id)) {
this.set({ id: window.getGuid(), e164: attributes.id });
}
this.ourNumber = textsecure.storage.user.getNumber();
this.ourUuid = textsecure.storage.user.getUuid();
this.verifiedEnum = textsecure.storage.protocol.VerifiedStatus;
// This may be overridden by ConversationController.getOrCreate, and signify
@ -148,7 +153,11 @@
},
isMe() {
return this.id === this.ourNumber;
const e164 = this.get('e164');
const uuid = this.get('uuid');
return (
(e164 && e164 === this.ourNumber) || (uuid && uuid === this.ourUuid)
);
},
hasDraft() {
@ -241,11 +250,17 @@
},
sendTypingMessage(isTyping) {
const groupId = !this.isPrivate() ? this.id : null;
const recipientId = this.isPrivate() ? this.id : null;
if (!textsecure.messaging) {
return;
}
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 sendOptions = this.getSendOptions();
this.wrapSend(
textsecure.messaging.sendTypingMessage(
{
@ -356,8 +371,6 @@
return this.cachedProps;
},
getProps() {
const { format } = PhoneNumber;
const regionCode = storage.get('regionCode');
const color = this.getColor();
const typingValues = _.values(this.contactTypingTimers || {});
@ -394,9 +407,7 @@
draftPreview,
draftText,
phoneNumber: format(this.id, {
ourRegionCode: regionCode,
}),
phoneNumber: this.getNumber(),
lastMessage: {
status: this.get('lastMessageStatus'),
text: this.get('lastMessage'),
@ -406,6 +417,31 @@
return result;
},
updateE164(e164) {
const oldValue = this.get('e164');
if (e164 !== oldValue) {
this.set('e164', e164);
window.Signal.Data.updateConversation(this.id, this.attributes);
this.trigger('idUpdated', this, 'e164', oldValue);
}
},
updateUuid(uuid) {
const oldValue = this.get('uuid');
if (uuid !== oldValue) {
this.set('uuid', uuid);
window.Signal.Data.updateConversation(this.id, this.attributes);
this.trigger('idUpdated', this, 'uuid', oldValue);
}
},
updateGroupId(groupId) {
const oldValue = this.get('groupId');
if (groupId !== oldValue) {
this.set('groupId', groupId);
window.Signal.Data.updateConversation(this.id, this.attributes);
this.trigger('idUpdated', this, 'groupId', oldValue);
}
},
onMessageError() {
this.updateVerified();
},
@ -506,24 +542,28 @@
});
}
if (!options.viaSyncMessage) {
await this.sendVerifySyncMessage(this.id, verified);
await this.sendVerifySyncMessage(
this.get('e164'),
this.get('uuid'),
verified
);
}
},
sendVerifySyncMessage(number, state) {
sendVerifySyncMessage(e164, uuid, state) {
// Because syncVerification sends a (null) message to the target of the verify and
// a sync message to our own devices, we need to send the accessKeys down for both
// contacts. So we merge their sendOptions.
const { sendOptions } = ConversationController.prepareForSend(
this.ourNumber,
this.ourNumber || this.ourUuid,
{ syncMessage: true }
);
const contactSendOptions = this.getSendOptions();
const options = Object.assign({}, sendOptions, contactSendOptions);
const promise = textsecure.storage.protocol.loadIdentityKey(number);
const promise = textsecure.storage.protocol.loadIdentityKey(e164);
return promise.then(key =>
this.wrapSend(
textsecure.messaging.syncVerification(number, state, key, options)
textsecure.messaging.syncVerification(e164, uuid, state, key, options)
)
);
},
@ -764,8 +804,8 @@
});
},
validate(attributes) {
const required = ['id', 'type'];
validate(attributes = this.attributes) {
const required = ['type'];
const missing = _.filter(required, attr => !attributes[attr]);
if (missing.length) {
return `Conversation must have ${missing}`;
@ -775,7 +815,16 @@
return `Invalid conversation type: ${attributes.type}`;
}
const error = this.validateNumber();
const atLeastOneOf = ['e164', 'uuid', 'groupId'];
const hasAtLeastOneOf =
_.filter(atLeastOneOf, attr => attributes[attr]).length > 0;
if (!hasAtLeastOneOf) {
return 'Missing one of e164, uuid, or groupId';
}
const error = this.validateNumber() || this.validateUuid();
if (error) {
return error;
}
@ -784,11 +833,14 @@
},
validateNumber() {
if (this.isPrivate()) {
if (this.isPrivate() && this.get('e164')) {
const regionCode = storage.get('regionCode');
const number = libphonenumber.util.parseNumber(this.id, regionCode);
const number = libphonenumber.util.parseNumber(
this.get('e164'),
regionCode
);
if (number.isValidNumber) {
this.set({ id: number.e164 });
this.set({ e164: number.e164 });
return null;
}
@ -798,6 +850,18 @@
return null;
},
validateUuid() {
if (this.isPrivate() && this.get('uuid')) {
if (window.isValidGuid(this.get('uuid'))) {
return null;
}
return 'Invalid UUID';
}
return null;
},
queueJob(callback) {
this.jobQueue = this.jobQueue || new window.PQueue({ concurrency: 1 });
@ -811,10 +875,15 @@
getRecipients() {
if (this.isPrivate()) {
return [this.id];
return [this.get('uuid') || this.get('e164')];
}
const me = textsecure.storage.user.getNumber();
return _.without(this.get('members'), me);
const me = ConversationController.getConversationId(
textsecure.storage.user.getUuid() || textsecure.storage.user.getNumber()
);
return _.without(this.get('members'), me).map(memberId => {
const c = ConversationController.get(memberId);
return c.get('uuid') || c.get('e164');
});
},
async getQuoteAttachment(attachments, preview, sticker) {
@ -908,7 +977,8 @@
: '';
return {
author: contact.id,
author: contact.get('e164'),
authorUuid: contact.get('uuid'),
id: quotedMessage.get('sent_at'),
text: body || embeddedContactName,
attachments: quotedMessage.isTapToView()
@ -955,7 +1025,9 @@
* @param {boolean} [reaction.remove] - Set to `true` if we are removing a
* reaction with the given emoji
* @param {object} target - The target of the reaction
* @param {string} target.targetAuthorE164 - The E164 address of the target
* @param {string} [target.targetAuthorE164] - The E164 address of the target
* message's author
* @param {string} [target.targetAuthorUuid] - The UUID address of the target
* message's author
* @param {number} target.targetTimestamp - The sent_at timestamp of the
* target message
@ -965,13 +1037,17 @@
const outgoingReaction = { ...reaction, ...target };
const reactionModel = Whisper.Reactions.add({
...outgoingReaction,
fromId: this.ourNumber || textsecure.storage.user.getNumber(),
fromId:
this.ourNumber ||
this.ourUuid ||
textsecure.storage.user.getNumber() ||
textsecure.storage.user.getUuid(),
timestamp,
fromSync: true,
});
Whisper.Reactions.onReaction(reactionModel);
const destination = this.id;
const destination = this.get('e164');
const recipients = this.getRecipients();
let profileKey;
@ -987,11 +1063,10 @@
timestamp
);
// Here we move attachments to disk
const attributes = {
id: window.getGuid(),
type: 'outgoing',
conversationId: destination,
conversationId: this.get('id'),
sent_at: timestamp,
received_at: timestamp,
recipients,
@ -1029,11 +1104,10 @@
}
const options = this.getSendOptions();
const groupNumbers = this.getRecipients();
const promise = (() => {
if (this.isPrivate()) {
return textsecure.messaging.sendMessageToNumber(
return textsecure.messaging.sendMessageToIdentifier(
destination,
null,
null,
@ -1049,8 +1123,8 @@
}
return textsecure.messaging.sendMessageToGroup(
destination,
groupNumbers,
this.get('groupId'),
this.getRecipients(),
null,
null,
null,
@ -1082,7 +1156,7 @@
const { clearUnreadMetrics } = window.reduxActions.conversations;
clearUnreadMetrics(this.id);
const destination = this.id;
const destination = this.get('uuid') || this.get('e164');
const expireTimer = this.get('expireTimer');
const recipients = this.getRecipients();
@ -1105,7 +1179,7 @@
const messageWithSchema = await upgradeMessageSchema({
type: 'outgoing',
body,
conversationId: destination,
conversationId: this.id,
quote,
preview,
attachments,
@ -1147,10 +1221,13 @@
// We're offline!
if (!textsecure.messaging) {
const errors = this.contactCollection.map(contact => {
const errors = (this.contactCollection.length
? this.contactCollection
: [this]
).map(contact => {
const error = new Error('Network is not available');
error.name = 'SendMessageNetworkError';
error.number = contact.id;
error.number = contact.get('uuid') || contact.get('e164');
return error;
});
await message.saveErrors(errors);
@ -1189,12 +1266,11 @@
const conversationType = this.get('type');
const options = this.getSendOptions();
const groupNumbers = this.getRecipients();
const promise = (() => {
switch (conversationType) {
case Message.PRIVATE:
return textsecure.messaging.sendMessageToNumber(
return textsecure.messaging.sendMessageToIdentifier(
destination,
messageBody,
finalAttachments,
@ -1209,8 +1285,8 @@
);
case Message.GROUP:
return textsecure.messaging.sendMessageToGroup(
destination,
groupNumbers,
this.get('groupId'),
this.getRecipients(),
messageBody,
finalAttachments,
quote,
@ -1239,7 +1315,7 @@
// success
if (result) {
await this.handleMessageSendResult(
result.failoverNumbers,
result.failoverIdentifiers,
result.unidentifiedDeliveries
);
}
@ -1249,7 +1325,7 @@
// failure
if (result) {
await this.handleMessageSendResult(
result.failoverNumbers,
result.failoverIdentifiers,
result.unidentifiedDeliveries
);
}
@ -1258,9 +1334,9 @@
);
},
async handleMessageSendResult(failoverNumbers, unidentifiedDeliveries) {
async handleMessageSendResult(failoverIdentifiers, unidentifiedDeliveries) {
await Promise.all(
(failoverNumbers || []).map(async number => {
(failoverIdentifiers || []).map(async number => {
const conversation = ConversationController.get(number);
if (
@ -1315,25 +1391,36 @@
getSendOptions(options = {}) {
const senderCertificate = storage.get('senderCertificate');
const numberInfo = this.getNumberInfo(options);
const senderCertificateWithUuid = storage.get(
'senderCertificateWithUuid'
);
const sendMetadata = this.getSendMetadata(options);
return {
senderCertificate,
numberInfo,
senderCertificateWithUuid,
sendMetadata,
};
},
getNumberInfo(options = {}) {
getUuidCapable() {
return Boolean(_.property('uuid')(this.get('capabilities')));
},
getSendMetadata(options = {}) {
const { syncMessage, disableMeCheck } = options;
if (!this.ourNumber) {
if (!this.ourNumber && !this.ourUuid) {
return null;
}
// START: this code has an Expiration date of ~2018/11/21
// We don't want to enable unidentified delivery for send unless it is
// also enabled for our own account.
const me = ConversationController.getOrCreate(this.ourNumber, 'private');
const me = ConversationController.getOrCreate(
this.ourNumber || this.ourUuid,
'private'
);
if (
!disableMeCheck &&
me.get('sealedSender') === SEALED_SENDER.DISABLED
@ -1344,29 +1431,36 @@
if (!this.isPrivate()) {
const infoArray = this.contactCollection.map(conversation =>
conversation.getNumberInfo(options)
conversation.getSendMetadata(options)
);
return Object.assign({}, ...infoArray);
}
const accessKey = this.get('accessKey');
const sealedSender = this.get('sealedSender');
const uuidCapable = this.getUuidCapable();
// We never send sync messages as sealed sender
if (syncMessage && this.id === this.ourNumber) {
if (syncMessage && this.isMe()) {
return null;
}
const e164 = this.get('e164');
const uuid = this.get('uuid');
// If we've never fetched user's profile, we default to what we have
if (sealedSender === SEALED_SENDER.UNKNOWN) {
const info = {
accessKey:
accessKey ||
window.Signal.Crypto.arrayBufferToBase64(
window.Signal.Crypto.getRandomBytes(16)
),
useUuidSenderCert: uuidCapable,
};
return {
[this.id]: {
accessKey:
accessKey ||
window.Signal.Crypto.arrayBufferToBase64(
window.Signal.Crypto.getRandomBytes(16)
),
},
...(e164 ? { [e164]: info } : {}),
...(uuid ? { [uuid]: info } : {}),
};
}
@ -1374,15 +1468,19 @@
return null;
}
const info = {
accessKey:
accessKey && sealedSender === SEALED_SENDER.ENABLED
? accessKey
: window.Signal.Crypto.arrayBufferToBase64(
window.Signal.Crypto.getRandomBytes(16)
),
useUuidSenderCert: uuidCapable,
};
return {
[this.id]: {
accessKey:
accessKey && sealedSender === SEALED_SENDER.ENABLED
? accessKey
: window.Signal.Crypto.arrayBufferToBase64(
window.Signal.Crypto.getRandomBytes(16)
),
},
...(e164 ? { [e164]: info } : {}),
...(uuid ? { [uuid]: info } : {}),
};
},
@ -1465,7 +1563,10 @@
source,
});
source = source || textsecure.storage.user.getNumber();
source =
source ||
textsecure.storage.user.getNumber() ||
textsecure.storage.user.getUuid();
// 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.
@ -1492,7 +1593,7 @@
});
if (this.isPrivate()) {
model.set({ destination: this.id });
model.set({ destination: this.get('uuid') || this.get('e164') });
}
if (model.isOutgoing()) {
model.set({ recipients: this.getRecipients() });
@ -1522,7 +1623,7 @@
const flags =
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
const dataMessage = await textsecure.messaging.getMessageProto(
this.get('id'),
this.get('uuid') || this.get('e164'),
null,
[],
null,
@ -1538,8 +1639,8 @@
}
if (this.get('type') === 'private') {
promise = textsecure.messaging.sendExpirationTimerUpdateToNumber(
this.get('id'),
promise = textsecure.messaging.sendExpirationTimerUpdateToIdentifier(
this.get('uuid') || this.get('e164'),
expireTimer,
message.get('sent_at'),
profileKey,
@ -1547,7 +1648,7 @@
);
} else {
promise = textsecure.messaging.sendExpirationTimerUpdateToGroup(
this.get('id'),
this.get('groupId'),
this.getRecipients(),
expireTimer,
message.get('sent_at'),
@ -1573,7 +1674,8 @@
type: 'outgoing',
sent_at: now,
received_at: now,
destination: this.id,
destination: this.get('e164'),
destinationUuid: this.get('uuid'),
recipients: this.getRecipients(),
flags: textsecure.protobuf.DataMessage.Flags.END_SESSION,
});
@ -1589,7 +1691,11 @@
const options = this.getSendOptions();
message.send(
this.wrapSend(
textsecure.messaging.resetSession(this.id, now, options)
textsecure.messaging.resetSession(
this.get('uuid') || this.get('e164'),
now,
options
)
)
);
}
@ -1720,7 +1826,7 @@
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
// to a contact, we need accessKeys for both.
const { sendOptions } = ConversationController.prepareForSend(
this.ourNumber,
this.ourUuid || this.ourNumber,
{ syncMessage: true }
);
await this.wrapSend(
@ -1731,11 +1837,13 @@
const convoSendOptions = this.getSendOptions();
await Promise.all(
_.map(_.groupBy(read, 'sender'), async (receipts, sender) => {
_.map(_.groupBy(read, 'sender'), async (receipts, identifier) => {
const timestamps = _.map(receipts, 'timestamp');
const c = ConversationController.get(identifier);
await this.wrapSend(
textsecure.messaging.sendReadReceipts(
sender,
c.get('e164'),
c.get('uuid'),
timestamps,
convoSendOptions
)
@ -1756,9 +1864,14 @@
// request all conversation members' keys
let ids = [];
if (this.isPrivate()) {
ids = [this.id];
ids = [this.get('uuid') || this.get('e164')];
} else {
ids = this.get('members');
ids = this.get('members')
.map(id => {
const c = ConversationController.get(id);
return c ? c.get('uuid') || c.get('e164') : null;
})
.filter(Boolean);
}
return Promise.all(_.map(ids, this.getProfile));
},
@ -1780,8 +1893,8 @@
try {
await c.deriveAccessKeyIfNeeded();
const numberInfo = c.getNumberInfo({ disableMeCheck: true }) || {};
const getInfo = numberInfo[c.id] || {};
const sendMetadata = c.getSendMetadata({ disableMeCheck: true }) || {};
const getInfo = sendMetadata[c.id] || {};
if (getInfo.accessKey) {
try {
@ -1863,6 +1976,10 @@
sealedSender: SEALED_SENDER.DISABLED,
});
}
if (profile.capabilities) {
c.set({ capabilities: profile.capabilities });
}
} catch (error) {
if (error.code !== 403 && error.code !== 404) {
window.log.error(
@ -2027,8 +2144,9 @@
this.set({ accessKey });
},
hasMember(number) {
return _.contains(this.get('members'), number);
hasMember(identifier) {
const cid = ConversationController.getConversationId(identifier);
return cid && _.contains(this.get('members'), cid);
},
fetchContacts() {
if (this.isPrivate()) {
@ -2114,7 +2232,7 @@
if (!this.isPrivate()) {
return '';
}
const number = this.id;
const number = this.get('e164');
try {
const parsedNumber = libphonenumber.parse(number);
const regionCode = libphonenumber.getRegionCodeForNumber(parsedNumber);
@ -2230,10 +2348,10 @@
},
notifyTyping(options = {}) {
const { isTyping, sender, senderDevice } = options;
const { isTyping, sender, senderUuid, senderDevice } = options;
// We don't do anything with typing messages from our other devices
if (sender === this.ourNumber) {
if (sender === this.ourNumber || senderUuid === this.ourUuid) {
return;
}
@ -2289,6 +2407,83 @@
Whisper.ConversationCollection = Backbone.Collection.extend({
model: Whisper.Conversation,
/**
* Backbone defines a `_byId` field. Here we set up additional `_byE164`,
* `_byUuid`, and `_byGroupId` fields so we can track conversations by more
* than just their id.
*/
initialize() {
this._byE164 = {};
this._byUuid = {};
this._byGroupId = {};
this.on('idUpdated', (model, idProp, oldValue) => {
if (oldValue) {
if (idProp === 'e164') {
delete this._byE164[oldValue];
}
if (idProp === 'uuid') {
delete this._byUuid[oldValue];
}
if (idProp === 'groupId') {
delete this._byGroupid[oldValue];
}
}
if (model.get('e164')) {
this._byE164[model.get('e164')] = model;
}
if (model.get('uuid')) {
this._byUuid[model.get('uuid')] = model;
}
if (model.get('groupId')) {
this._byGroupid[model.get('groupId')] = model;
}
});
},
reset(...args) {
Backbone.Collection.prototype.reset.apply(this, args);
this._byE164 = {};
this._byUuid = {};
this._byGroupId = {};
},
add(...models) {
const res = Backbone.Collection.prototype.add.apply(this, models);
[].concat(res).forEach(model => {
const e164 = model.get('e164');
if (e164) {
this._byE164[e164] = model;
}
const uuid = model.get('uuid');
if (uuid) {
this._byUuid[uuid] = model;
}
const groupId = model.get('groupId');
if (groupId) {
this._byGroupId[groupId] = model;
}
});
return res;
},
/**
* Backbone collections have a `_byId` field that `get` defers to. Here, we
* override `get` to first access our custom `_byE164`, `_byUuid`, and
* `_byGroupId` functions, followed by falling back to the original
* Backbone implementation.
*/
get(id) {
return (
this._byE164[id] ||
this._byE164[`+${id}`] ||
this._byUuid[id] ||
this._byGroupId[id] ||
Backbone.Collection.prototype.get.call(this, id)
);
},
comparator(m) {
return -m.get('timestamp');
},

View file

@ -78,6 +78,9 @@
window.AccountCache[number] !== undefined;
window.hasSignalAccount = number => window.AccountCache[number];
const includesAny = (haystack, ...needles) =>
needles.some(needle => haystack.includes(needle));
window.Whisper.Message = Backbone.Model.extend({
initialize(attributes) {
if (_.isObject(attributes)) {
@ -94,6 +97,7 @@
this.INITIAL_PROTOCOL_VERSION =
textsecure.protobuf.DataMessage.ProtocolVersion.INITIAL;
this.OUR_NUMBER = textsecure.storage.user.getNumber();
this.OUR_UUID = textsecure.storage.user.getUuid();
this.on('destroy', this.onDestroy);
this.on('change:expirationStartTimestamp', this.setToExpire);
@ -178,24 +182,32 @@
// Other top-level prop-generation
getPropsForSearchResult() {
const fromNumber = this.getSource();
const from = this.findAndFormatContact(fromNumber);
if (fromNumber === this.OUR_NUMBER) {
from.isMe = true;
const sourceE164 = this.getSource();
const sourceUuid = this.getSourceUuid();
const fromContact = this.findAndFormatContact(sourceE164 || sourceUuid);
if (
(sourceE164 && sourceE164 === this.OUR_NUMBER) ||
(sourceUuid && sourceUuid === this.OUR_UUID)
) {
fromContact.isMe = true;
}
const toNumber = this.get('conversationId');
let to = this.findAndFormatContact(toNumber);
if (toNumber === this.OUR_NUMBER) {
const conversation = this.getConversation();
let to = this.findAndFormatContact(conversation.get('id'));
if (conversation.isMe()) {
to.isMe = true;
} else if (fromNumber === toNumber) {
} else if (
sourceE164 === conversation.get('e164') ||
sourceUuid === conversation.get('uuid')
) {
to = {
isMe: true,
};
}
return {
from,
from: fromContact,
to,
isSelected: this.isSelected,
@ -221,11 +233,15 @@
// We include numbers we didn't successfully send to so we can display errors.
// Older messages don't have the recipients included on the message, so we fall
// back to the conversation's current recipients
const phoneNumbers = this.isIncoming()
? [this.get('source')]
const conversationIds = this.isIncoming()
? [this.getConversation().get('id')]
: _.union(
this.get('sent_to') || [],
this.get('recipients') || this.getConversation().getRecipients()
(this.get('sent_to') || []).map(id =>
ConversationController.getConversationId(id)
),
(
this.get('recipients') || this.getConversation().getRecipients()
).map(id => ConversationController.getConversationId(id))
);
// This will make the error message for outgoing key errors a bit nicer
@ -242,7 +258,7 @@
// that contact. Otherwise, it will be a standalone entry.
const errors = _.reject(allErrors, error => Boolean(error.number));
const errorsGroupedById = _.groupBy(allErrors, 'number');
const finalContacts = (phoneNumbers || []).map(id => {
const finalContacts = (conversationIds || []).map(id => {
const errorsForContact = errorsGroupedById[id];
const isOutgoingKeyError = Boolean(
_.find(errorsForContact, error => error.name === OUTGOING_KEY_ERROR)
@ -353,7 +369,7 @@
return null;
}
const { expireTimer, fromSync, source } = timerUpdate;
const { expireTimer, fromSync, source, sourceUuid } = timerUpdate;
const timespan = Whisper.ExpirationTimerOptions.getName(expireTimer || 0);
const disabled = !expireTimer;
@ -369,7 +385,7 @@
...basicProps,
type: 'fromSync',
};
} else if (source === this.OUR_NUMBER) {
} else if (source === this.OUR_NUMBER || sourceUuid === this.OUR_UUID) {
return {
...basicProps,
type: 'fromMe',
@ -477,9 +493,10 @@
.map(attachment => this.getPropsForAttachment(attachment));
},
getPropsForMessage() {
const phoneNumber = this.getSource();
const contact = this.findAndFormatContact(phoneNumber);
const contactModel = this.findContact(phoneNumber);
const sourceE164 = this.getSource();
const sourceUuid = this.getSourceUuid();
const contact = this.findAndFormatContact(sourceE164 || sourceUuid);
const contactModel = this.findContact(sourceE164 || sourceUuid);
const authorColor = contactModel ? contactModel.getColor() : null;
const authorAvatarPath = contactModel
@ -558,8 +575,8 @@
},
// Dependencies of prop-generation functions
findAndFormatContact(phoneNumber) {
const contactModel = this.findContact(phoneNumber);
findAndFormatContact(identifier) {
const contactModel = this.findContact(identifier);
if (contactModel) {
return contactModel.format();
}
@ -567,13 +584,13 @@
const { format } = PhoneNumber;
const regionCode = storage.get('regionCode');
return {
phoneNumber: format(phoneNumber, {
phoneNumber: format(identifier, {
ourRegionCode: regionCode,
}),
};
},
findContact(phoneNumber) {
return ConversationController.get(phoneNumber);
findContact(identifier) {
return ConversationController.get(identifier);
},
getConversation() {
// This needs to be an unsafe call, because this method is called during
@ -700,8 +717,14 @@
const { format } = PhoneNumber;
const regionCode = storage.get('regionCode');
const { author, id: sentAt, referencedMessageNotFound } = quote;
const contact = author && ConversationController.get(author);
const {
author,
authorUuid,
id: sentAt,
referencedMessageNotFound,
} = quote;
const contact =
author && ConversationController.get(author || authorUuid);
const authorColor = contact ? contact.getColor() : 'grey';
const authorPhoneNumber = format(author, {
@ -709,7 +732,7 @@
});
const authorProfileName = contact ? contact.getProfileName() : null;
const authorName = contact ? contact.getName() : null;
const isFromMe = contact ? contact.id === this.OUR_NUMBER : false;
const isFromMe = contact ? contact.isMe() : false;
const firstAttachment = quote.attachments && quote.attachments[0];
return {
@ -728,17 +751,26 @@
onClick: () => this.trigger('scroll-to-message'),
};
},
getStatus(number) {
getStatus(identifier) {
const conversation = ConversationController.get(identifier);
if (!conversation) {
return null;
}
const e164 = conversation.get('e164');
const uuid = conversation.get('uuid');
const readBy = this.get('read_by') || [];
if (readBy.indexOf(number) >= 0) {
if (includesAny(readBy, identifier, e164, uuid)) {
return 'read';
}
const deliveredTo = this.get('delivered_to') || [];
if (deliveredTo.indexOf(number) >= 0) {
if (includesAny(deliveredTo, identifier, e164, uuid)) {
return 'delivered';
}
const sentTo = this.get('sent_to') || [];
if (sentTo.indexOf(number) >= 0) {
if (includesAny(sentTo, identifier, e164, uuid)) {
return 'sent';
}
@ -982,17 +1014,24 @@
if (!fromSync) {
const sender = this.getSource();
const senderUuid = this.getSourceUuid();
const timestamp = this.get('sent_at');
const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const { wrap, sendOptions } = ConversationController.prepareForSend(
ourNumber,
ourNumber || ourUuid,
{
syncMessage: true,
}
);
await wrap(
textsecure.messaging.syncViewOnceOpen(sender, timestamp, sendOptions)
textsecure.messaging.syncViewOnceOpen(
sender,
senderUuid,
timestamp,
sendOptions
)
);
}
},
@ -1052,14 +1091,25 @@
return this.OUR_NUMBER;
},
getSourceUuid() {
if (this.isIncoming()) {
return this.get('sourceUuid');
}
return this.OUR_UUID;
},
getContact() {
const source = this.getSource();
const sourceUuid = this.getSourceUuid();
if (!source) {
if (!source && !sourceUuid) {
return null;
}
return ConversationController.getOrCreate(source, 'private');
return ConversationController.getOrCreate(
source || sourceUuid,
'private'
);
},
isOutgoing() {
return this.get('type') === 'outgoing';
@ -1237,9 +1287,9 @@
// Special-case the self-send case - we send only a sync message
if (recipients.length === 1 && recipients[0] === this.OUR_NUMBER) {
const [number] = recipients;
const [identifier] = recipients;
const dataMessage = await textsecure.messaging.getMessageProto(
number,
identifier,
body,
attachments,
quoteWithData,
@ -1257,9 +1307,9 @@
const options = conversation.getSendOptions();
if (conversation.isPrivate()) {
const [number] = recipients;
promise = textsecure.messaging.sendMessageToNumber(
number,
const [identifer] = recipients;
promise = textsecure.messaging.sendMessageToIdentifier(
identifer,
body,
attachments,
quoteWithData,
@ -1327,8 +1377,8 @@
// Called when the user ran into an error with a specific user, wants to send to them
// One caller today: ConversationView.forceSend()
async resend(number) {
const error = this.removeOutgoingErrors(number);
async resend(identifier) {
const error = this.removeOutgoingErrors(identifier);
if (!error) {
window.log.warn('resend: requested number was not present in errors');
return null;
@ -1349,9 +1399,9 @@
const stickerWithData = await loadStickerData(this.get('sticker'));
// Special-case the self-send case - we send only a sync message
if (number === this.OUR_NUMBER) {
if (identifier === this.OUR_NUMBER || identifier === this.OUR_UUID) {
const dataMessage = await textsecure.messaging.getMessageProto(
number,
identifier,
body,
attachments,
quoteWithData,
@ -1366,10 +1416,10 @@
}
const { wrap, sendOptions } = ConversationController.prepareForSend(
number
identifier
);
const promise = textsecure.messaging.sendMessageToNumber(
number,
identifier,
body,
attachments,
quoteWithData,
@ -1411,7 +1461,7 @@
const sentTo = this.get('sent_to') || [];
this.set({
sent_to: _.union(sentTo, result.successfulNumbers),
sent_to: _.union(sentTo, result.successfulIdentifiers),
sent: true,
expirationStartTimestamp: Date.now(),
unidentifiedDeliveries: result.unidentifiedDeliveries,
@ -1442,7 +1492,7 @@
promises.push(c.getProfiles());
}
} else {
if (result.successfulNumbers.length > 0) {
if (result.successfulIdentifiers.length > 0) {
const sentTo = this.get('sent_to') || [];
// In groups, we don't treat unregistered users as a user-visible
@ -1462,7 +1512,7 @@
this.saveErrors(filteredErrors);
this.set({
sent_to: _.union(sentTo, result.successfulNumbers),
sent_to: _.union(sentTo, result.successfulIdentifiers),
sent: true,
expirationStartTimestamp,
unidentifiedDeliveries: result.unidentifiedDeliveries,
@ -1488,12 +1538,13 @@
},
async sendSyncMessageOnly(dataMessage) {
const conv = this.getConversation();
this.set({ dataMessage });
try {
this.set({
// These are the same as a normal send()
sent_to: [this.OUR_NUMBER],
sent_to: [conv.get('uuid') || conv.get('e164')],
sent: true,
expirationStartTimestamp: Date.now(),
});
@ -1503,8 +1554,8 @@
unidentifiedDeliveries: result ? result.unidentifiedDeliveries : null,
// These are unique to a Note to Self message - immediately read/delivered
delivered_to: [this.OUR_NUMBER],
read_by: [this.OUR_NUMBER],
delivered_to: [this.OUR_UUID || this.OUR_NUMBER],
read_by: [this.OUR_UUID || this.OUR_NUMBER],
});
} catch (result) {
const errors = (result && result.errors) || [
@ -1528,8 +1579,9 @@
sendSyncMessage() {
const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const { wrap, sendOptions } = ConversationController.prepareForSend(
ourNumber,
ourUuid || ourNumber,
{
syncMessage: true,
}
@ -1542,12 +1594,14 @@
return Promise.resolve();
}
const isUpdate = Boolean(this.get('synced'));
const conv = this.getConversation();
return wrap(
textsecure.messaging.sendSyncMessage(
dataMessage,
this.get('sent_at'),
this.get('destination'),
conv.get('e164'),
conv.get('uuid'),
this.get('expirationStartTimestamp'),
this.get('sent_to'),
this.get('unidentifiedDeliveries'),
@ -1773,7 +1827,11 @@
const found = collection.find(item => {
const messageAuthor = item.getContact();
return messageAuthor && author === messageAuthor.id;
return (
messageAuthor &&
ConversationController.getConversationId(author) ===
messageAuthor.get('id')
);
});
if (!found) {
@ -1873,6 +1931,7 @@
// still go through one of the previous two codepaths
const message = this;
const source = message.get('source');
const sourceUuid = message.get('sourceUuid');
const type = message.get('type');
let conversationId = message.get('conversationId');
if (initialMessage.group) {
@ -1952,6 +2011,7 @@
// We drop incoming messages for groups we already know about, which we're not a
// part of, except for group updates.
const ourUuid = textsecure.storage.user.getUuid();
const ourNumber = textsecure.storage.user.getNumber();
const isGroupUpdate =
initialMessage.group &&
@ -1960,7 +2020,7 @@
if (
type === 'incoming' &&
!conversation.isPrivate() &&
!conversation.hasMember(ourNumber) &&
!conversation.hasMember(ourNumber || ourUuid) &&
!isGroupUpdate
) {
window.log.warn(
@ -1982,6 +2042,7 @@
Whisper.deliveryReceiptQueue.add(() => {
Whisper.deliveryReceiptBatcher.add({
source,
sourceUuid,
timestamp: this.get('sent_at'),
});
});
@ -2044,6 +2105,20 @@
};
if (dataMessage.group) {
let groupUpdate = null;
const memberConversations = await Promise.all(
(
dataMessage.group.members || dataMessage.group.membersE164
).map(member => {
if (member.e164 || member.uuid) {
return ConversationController.getOrCreateAndWait(
member.e164 || member.uuid,
'private'
);
}
return ConversationController.getOrCreateAndWait(member);
})
);
const members = memberConversations.map(c => c.get('id'));
attributes = {
...attributes,
type: 'group',
@ -2053,10 +2128,7 @@
attributes = {
...attributes,
name: dataMessage.group.name,
members: _.union(
dataMessage.group.members,
conversation.get('members')
),
members: _.union(members, conversation.get('members')),
};
groupUpdate =
@ -2065,7 +2137,7 @@
) || {};
const difference = _.difference(
attributes.members,
members,
conversation.get('members')
);
if (difference.length > 0) {
@ -2076,15 +2148,22 @@
attributes.left = false;
}
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
if (source === textsecure.storage.user.getNumber()) {
if (
source === textsecure.storage.user.getNumber() ||
sourceUuid === textsecure.storage.user.getUuid()
) {
attributes.left = true;
groupUpdate = { left: 'You' };
} else {
groupUpdate = { left: source };
const myConversation = ConversationController.get(
source || sourceUuid
);
groupUpdate = { left: myConversation.get('id') };
}
attributes.members = _.without(
conversation.get('members'),
source
source,
sourceUuid
);
}
@ -2102,7 +2181,7 @@
message.set({
delivered: (message.get('delivered') || 0) + 1,
delivered_to: _.union(message.get('delivered_to') || [], [
receipt.get('source'),
receipt.get('deliveredTo'),
]),
})
);
@ -2216,13 +2295,16 @@
if (dataMessage.profileKey) {
const profileKey = dataMessage.profileKey.toString('base64');
if (source === textsecure.storage.user.getNumber()) {
if (
source === textsecure.storage.user.getNumber() ||
sourceUuid === textsecure.storage.user.getUuid()
) {
conversation.set({ profileSharing: true });
} else if (conversation.isPrivate()) {
conversation.setProfileKey(profileKey);
} else {
ConversationController.getOrCreateAndWait(
source,
source || sourceUuid,
'private'
).then(sender => {
sender.setProfileKey(profileKey);