Passive UUID support
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
f64ca0ed21
commit
a90246cbe5
49 changed files with 2226 additions and 776 deletions
253
js/background.js
253
js/background.js
|
@ -25,7 +25,7 @@
|
|||
wait: 500,
|
||||
maxSize: 500,
|
||||
processBatch: async items => {
|
||||
const bySource = _.groupBy(items, item => item.source);
|
||||
const bySource = _.groupBy(items, item => item.source || item.sourceUuid);
|
||||
const sources = Object.keys(bySource);
|
||||
|
||||
for (let i = 0, max = sources.length; i < max; i += 1) {
|
||||
|
@ -33,13 +33,15 @@
|
|||
const timestamps = bySource[source].map(item => item.timestamp);
|
||||
|
||||
try {
|
||||
const c = ConversationController.get(source);
|
||||
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||
source
|
||||
c.get('id')
|
||||
);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await wrap(
|
||||
textsecure.messaging.sendDeliveryReceipt(
|
||||
source,
|
||||
c.get('e164'),
|
||||
c.get('uuid'),
|
||||
timestamps,
|
||||
sendOptions
|
||||
)
|
||||
|
@ -234,13 +236,23 @@
|
|||
let accountManager;
|
||||
window.getAccountManager = () => {
|
||||
if (!accountManager) {
|
||||
const USERNAME = storage.get('number_id');
|
||||
const OLD_USERNAME = storage.get('number_id');
|
||||
const USERNAME = storage.get('uuid_id');
|
||||
const PASSWORD = storage.get('password');
|
||||
accountManager = new textsecure.AccountManager(USERNAME, PASSWORD);
|
||||
accountManager = new textsecure.AccountManager(
|
||||
USERNAME || OLD_USERNAME,
|
||||
PASSWORD
|
||||
);
|
||||
accountManager.addEventListener('registration', () => {
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const ourUuid = textsecure.storage.user.getUuid();
|
||||
const user = {
|
||||
regionCode: window.storage.get('regionCode'),
|
||||
ourNumber: textsecure.storage.user.getNumber(),
|
||||
ourNumber,
|
||||
ourUuid,
|
||||
ourConversationId: ConversationController.getConversationId(
|
||||
ourNumber || ourUuid
|
||||
),
|
||||
};
|
||||
Whisper.events.trigger('userChanged', user);
|
||||
|
||||
|
@ -580,6 +592,11 @@
|
|||
const conversations = convoCollection.map(
|
||||
conversation => conversation.cachedProps
|
||||
);
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const ourUuid = textsecure.storage.user.getUuid();
|
||||
const ourConversationId = ConversationController.getConversationId(
|
||||
ourNumber || ourUuid
|
||||
);
|
||||
const initialState = {
|
||||
conversations: {
|
||||
conversationLookup: Signal.Util.makeLookup(conversations, 'id'),
|
||||
|
@ -598,7 +615,9 @@
|
|||
stickersPath: window.baseStickersPath,
|
||||
tempPath: window.baseTempPath,
|
||||
regionCode: window.storage.get('regionCode'),
|
||||
ourNumber: textsecure.storage.user.getNumber(),
|
||||
ourConversationId,
|
||||
ourNumber,
|
||||
ourUuid,
|
||||
platform: window.platform,
|
||||
i18n: window.i18n,
|
||||
interactionMode: window.getInteractionMode(),
|
||||
|
@ -1508,7 +1527,8 @@
|
|||
messageReceiver = null;
|
||||
}
|
||||
|
||||
const USERNAME = storage.get('number_id');
|
||||
const OLD_USERNAME = storage.get('number_id');
|
||||
const USERNAME = storage.get('uuid_id');
|
||||
const PASSWORD = storage.get('password');
|
||||
const mySignalingKey = storage.get('signaling_key');
|
||||
|
||||
|
@ -1524,6 +1544,7 @@
|
|||
// initialize the socket and start listening for messages
|
||||
window.log.info('Initializing socket and listening for messages');
|
||||
messageReceiver = new textsecure.MessageReceiver(
|
||||
OLD_USERNAME,
|
||||
USERNAME,
|
||||
PASSWORD,
|
||||
mySignalingKey,
|
||||
|
@ -1569,7 +1590,7 @@
|
|||
});
|
||||
|
||||
window.textsecure.messaging = new textsecure.MessageSender(
|
||||
USERNAME,
|
||||
USERNAME || OLD_USERNAME,
|
||||
PASSWORD
|
||||
);
|
||||
|
||||
|
@ -1605,7 +1626,10 @@
|
|||
|
||||
const udSupportKey = 'hasRegisterSupportForUnauthenticatedDelivery';
|
||||
if (!storage.get(udSupportKey)) {
|
||||
const server = WebAPI.connect({ username: USERNAME, password: PASSWORD });
|
||||
const server = WebAPI.connect({
|
||||
username: USERNAME || OLD_USERNAME,
|
||||
password: PASSWORD,
|
||||
});
|
||||
try {
|
||||
await server.registerSupportForUnauthenticatedDelivery();
|
||||
storage.put(udSupportKey, true);
|
||||
|
@ -1617,7 +1641,50 @@
|
|||
}
|
||||
}
|
||||
|
||||
const hasRegisteredUuidSupportKey = 'hasRegisteredUuidSupport';
|
||||
if (
|
||||
!storage.get(hasRegisteredUuidSupportKey) &&
|
||||
textsecure.storage.user.getUuid()
|
||||
) {
|
||||
const server = WebAPI.connect({
|
||||
username: USERNAME || OLD_USERNAME,
|
||||
password: PASSWORD,
|
||||
});
|
||||
try {
|
||||
await server.registerCapabilities({ uuid: true });
|
||||
storage.put(hasRegisteredUuidSupportKey, true);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Error: Unable to register support for UUID messages.',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const deviceId = textsecure.storage.user.getDeviceId();
|
||||
|
||||
if (!textsecure.storage.user.getUuid()) {
|
||||
const server = WebAPI.connect({
|
||||
username: OLD_USERNAME,
|
||||
password: PASSWORD,
|
||||
});
|
||||
try {
|
||||
const { uuid } = await server.whoami();
|
||||
textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId);
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const me = await ConversationController.getOrCreateAndWait(
|
||||
ourNumber,
|
||||
'private'
|
||||
);
|
||||
me.updateUuid(uuid);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Error: Unable to retrieve UUID from service.',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (firstRun === true && deviceId !== '1') {
|
||||
const hasThemeSetting = Boolean(storage.get('theme-setting'));
|
||||
if (!hasThemeSetting && textsecure.storage.get('userAgent') === 'OWI') {
|
||||
|
@ -1639,9 +1706,10 @@
|
|||
Whisper.events.trigger('contactsync');
|
||||
});
|
||||
|
||||
const ourUuid = textsecure.storage.user.getUuid();
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||
ourNumber,
|
||||
ourNumber || ourUuid,
|
||||
{
|
||||
syncMessage: true,
|
||||
}
|
||||
|
@ -1766,7 +1834,7 @@
|
|||
function onTyping(ev) {
|
||||
// Note: this type of message is automatically removed from cache in MessageReceiver
|
||||
|
||||
const { typing, sender, senderDevice } = ev;
|
||||
const { typing, sender, senderUuid, senderDevice } = ev;
|
||||
const { groupId, started } = typing || {};
|
||||
|
||||
// We don't do anything with incoming typing messages if the setting is disabled
|
||||
|
@ -1774,12 +1842,18 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const conversation = ConversationController.get(groupId || sender);
|
||||
const conversation = ConversationController.get(
|
||||
groupId || sender || senderUuid
|
||||
);
|
||||
const ourUuid = textsecure.storage.user.getUuid();
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
|
||||
if (conversation) {
|
||||
// We drop typing notifications in groups we're not a part of
|
||||
if (!conversation.isPrivate() && !conversation.hasMember(ourNumber)) {
|
||||
if (
|
||||
!conversation.isPrivate() &&
|
||||
!conversation.hasMember(ourNumber || ourUuid)
|
||||
) {
|
||||
window.log.warn(
|
||||
`Received typing indicator for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
|
||||
);
|
||||
|
@ -1789,6 +1863,7 @@
|
|||
conversation.notifyTyping({
|
||||
isTyping: started,
|
||||
sender,
|
||||
senderUuid,
|
||||
senderDevice,
|
||||
});
|
||||
}
|
||||
|
@ -1833,9 +1908,10 @@
|
|||
async function onContactReceived(ev) {
|
||||
const details = ev.contactDetails;
|
||||
|
||||
const id = details.number;
|
||||
|
||||
if (id === textsecure.storage.user.getNumber()) {
|
||||
if (
|
||||
details.number === textsecure.storage.user.getNumber() ||
|
||||
details.uuid === textsecure.storage.user.getUuid()
|
||||
) {
|
||||
// special case for syncing details about ourselves
|
||||
if (details.profileKey) {
|
||||
window.log.info('Got sync message with our own profile key');
|
||||
|
@ -1844,9 +1920,11 @@
|
|||
}
|
||||
|
||||
const c = new Whisper.Conversation({
|
||||
id,
|
||||
e164: details.number,
|
||||
uuid: details.uuid,
|
||||
type: 'private',
|
||||
});
|
||||
const validationError = c.validateNumber();
|
||||
const validationError = c.validate();
|
||||
if (validationError) {
|
||||
window.log.error(
|
||||
'Invalid contact received:',
|
||||
|
@ -1857,7 +1935,7 @@
|
|||
|
||||
try {
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
id,
|
||||
details.number || details.uuid,
|
||||
'private'
|
||||
);
|
||||
let activeAt = conversation.get('active_at');
|
||||
|
@ -1878,10 +1956,18 @@
|
|||
}
|
||||
|
||||
if (typeof details.blocked !== 'undefined') {
|
||||
if (details.blocked) {
|
||||
storage.addBlockedNumber(id);
|
||||
const e164 = conversation.get('e164');
|
||||
if (details.blocked && e164) {
|
||||
storage.addBlockedNumber(e164);
|
||||
} else {
|
||||
storage.removeBlockedNumber(id);
|
||||
storage.removeBlockedNumber(e164);
|
||||
}
|
||||
|
||||
const uuid = conversation.get('uuid');
|
||||
if (details.blocked && uuid) {
|
||||
storage.addBlockedUuid(uuid);
|
||||
} else {
|
||||
storage.removeBlockedUuid(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1912,17 +1998,21 @@
|
|||
conversation.set({ avatar: null });
|
||||
}
|
||||
|
||||
window.Signal.Data.updateConversation(id, conversation.attributes);
|
||||
window.Signal.Data.updateConversation(
|
||||
details.number || details.uuid,
|
||||
conversation.attributes
|
||||
);
|
||||
|
||||
const { expireTimer } = details;
|
||||
const isValidExpireTimer = typeof expireTimer === 'number';
|
||||
if (isValidExpireTimer) {
|
||||
const source = textsecure.storage.user.getNumber();
|
||||
const sourceE164 = textsecure.storage.user.getNumber();
|
||||
const sourceUuid = textsecure.storage.user.getUuid();
|
||||
const receivedAt = Date.now();
|
||||
|
||||
await conversation.updateExpirationTimer(
|
||||
expireTimer,
|
||||
source,
|
||||
sourceE164 || sourceUuid,
|
||||
receivedAt,
|
||||
{ fromSync: true }
|
||||
);
|
||||
|
@ -1934,6 +2024,7 @@
|
|||
verifiedEvent.verified = {
|
||||
state: verified.state,
|
||||
destination: verified.destination,
|
||||
destinationUuid: verified.destinationUuid,
|
||||
identityKey: verified.identityKey.toArrayBuffer(),
|
||||
};
|
||||
verifiedEvent.viaContactSync = true;
|
||||
|
@ -1953,9 +2044,23 @@
|
|||
'group'
|
||||
);
|
||||
|
||||
const memberConversations = await Promise.all(
|
||||
(details.members || details.membersE164).map(member => {
|
||||
if (member.e164 || member.uuid) {
|
||||
return ConversationController.getOrCreateAndWait(
|
||||
member.e164 || member.uuid,
|
||||
'private'
|
||||
);
|
||||
}
|
||||
return ConversationController.getOrCreateAndWait(member, 'private');
|
||||
})
|
||||
);
|
||||
|
||||
const members = memberConversations.map(c => c.get('id'));
|
||||
|
||||
const updates = {
|
||||
name: details.name,
|
||||
members: details.members,
|
||||
members,
|
||||
color: details.color,
|
||||
type: 'group',
|
||||
};
|
||||
|
@ -2004,11 +2109,17 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const source = textsecure.storage.user.getNumber();
|
||||
const sourceE164 = textsecure.storage.user.getNumber();
|
||||
const sourceUuid = textsecure.storage.user.getUuid();
|
||||
const receivedAt = Date.now();
|
||||
await conversation.updateExpirationTimer(expireTimer, source, receivedAt, {
|
||||
fromSync: true,
|
||||
});
|
||||
await conversation.updateExpirationTimer(
|
||||
expireTimer,
|
||||
sourceE164 || sourceUuid,
|
||||
receivedAt,
|
||||
{
|
||||
fromSync: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Descriptors
|
||||
|
@ -2024,10 +2135,10 @@
|
|||
: { type: Message.PRIVATE, id: destination };
|
||||
|
||||
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
|
||||
const getDescriptorForReceived = ({ message, source }) =>
|
||||
const getDescriptorForReceived = ({ message, source, sourceUuid }) =>
|
||||
message.group
|
||||
? getGroupDescriptor(message.group)
|
||||
: { type: Message.PRIVATE, id: source };
|
||||
: { type: Message.PRIVATE, id: source || sourceUuid };
|
||||
|
||||
// Received:
|
||||
async function handleMessageReceivedProfileUpdate({
|
||||
|
@ -2069,11 +2180,18 @@
|
|||
|
||||
const message = await initIncomingMessage(data);
|
||||
|
||||
await ConversationController.getOrCreateAndWait(
|
||||
const result = await ConversationController.getOrCreateAndWait(
|
||||
messageDescriptor.id,
|
||||
messageDescriptor.type
|
||||
);
|
||||
|
||||
if (messageDescriptor.type === 'private') {
|
||||
result.updateE164(data.source);
|
||||
if (data.sourceUuid) {
|
||||
result.updateUuid(data.sourceUuid);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.message.reaction) {
|
||||
const { reaction } = data.message;
|
||||
const reactionModel = Whisper.Reactions.add({
|
||||
|
@ -2083,7 +2201,7 @@
|
|||
targetAuthorUuid: reaction.targetAuthorUuid,
|
||||
targetTimestamp: reaction.targetTimestamp.toNumber(),
|
||||
timestamp: Date.now(),
|
||||
fromId: data.source,
|
||||
fromId: data.source || data.sourceUuid,
|
||||
});
|
||||
// Note: We do not wait for completion here
|
||||
Whisper.Reactions.onReaction(reactionModel);
|
||||
|
@ -2112,8 +2230,12 @@
|
|||
|
||||
// Then we update our own profileKey if it's different from what we have
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const ourUuid = textsecure.storage.user.getUuid();
|
||||
const profileKey = data.message.profileKey.toString('base64');
|
||||
const me = await ConversationController.getOrCreate(ourNumber, 'private');
|
||||
const me = await ConversationController.getOrCreate(
|
||||
ourNumber || ourUuid,
|
||||
'private'
|
||||
);
|
||||
|
||||
// Will do the save for us if needed
|
||||
await me.setProfileKey(profileKey);
|
||||
|
@ -2136,6 +2258,7 @@
|
|||
|
||||
return new Whisper.Message({
|
||||
source: textsecure.storage.user.getNumber(),
|
||||
sourceUuid: textsecure.storage.user.getUuid(),
|
||||
sourceDevice: data.device,
|
||||
sent_at: data.timestamp,
|
||||
sent_to: sentTo,
|
||||
|
@ -2176,6 +2299,7 @@
|
|||
if (data.message.reaction) {
|
||||
const { reaction } = data.message;
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const ourUuid = textsecure.storage.user.getUuid();
|
||||
const reactionModel = Whisper.Reactions.add({
|
||||
emoji: reaction.emoji,
|
||||
remove: reaction.remove,
|
||||
|
@ -2183,7 +2307,7 @@
|
|||
targetAuthorUuid: reaction.targetAuthorUuid,
|
||||
targetTimestamp: reaction.targetTimestamp.toNumber(),
|
||||
timestamp: Date.now(),
|
||||
fromId: ourNumber,
|
||||
fromId: ourNumber || ourUuid,
|
||||
fromSync: true,
|
||||
});
|
||||
// Note: We do not wait for completion here
|
||||
|
@ -2197,20 +2321,25 @@
|
|||
messageDescriptor.id,
|
||||
messageDescriptor.type
|
||||
);
|
||||
|
||||
// Don't wait for handleDataMessage, as it has its own per-conversation queueing
|
||||
|
||||
message.handleDataMessage(data.message, event.confirm, {
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async function initIncomingMessage(data) {
|
||||
const targetId = data.source || data.sourceUuid;
|
||||
const conversation = ConversationController.get(targetId);
|
||||
const conversationId = conversation ? conversation.id : targetId;
|
||||
|
||||
return new Whisper.Message({
|
||||
source: data.source,
|
||||
sourceUuid: data.sourceUuid,
|
||||
sourceDevice: data.sourceDevice,
|
||||
sent_at: data.timestamp,
|
||||
received_at: data.receivedAt || Date.now(),
|
||||
conversationId: data.source,
|
||||
conversationId,
|
||||
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
||||
type: 'incoming',
|
||||
unread: 1,
|
||||
|
@ -2384,11 +2513,16 @@
|
|||
async function onViewSync(ev) {
|
||||
ev.confirm();
|
||||
|
||||
const { source, timestamp } = ev;
|
||||
const { source, sourceUuid, timestamp } = ev;
|
||||
window.log.info(`view sync ${source} ${timestamp}`);
|
||||
const conversationId = ConversationController.getConversationId(
|
||||
source || sourceUuid
|
||||
);
|
||||
|
||||
const sync = Whisper.ViewSyncs.add({
|
||||
source,
|
||||
sourceUuid,
|
||||
conversationId,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
|
@ -2398,12 +2532,12 @@
|
|||
function onReadReceipt(ev) {
|
||||
const readAt = ev.timestamp;
|
||||
const { timestamp } = ev.read;
|
||||
const { reader } = ev.read;
|
||||
const reader = ConversationController.getConversationId(ev.read.reader);
|
||||
window.log.info('read receipt', reader, timestamp);
|
||||
|
||||
ev.confirm();
|
||||
|
||||
if (!storage.get('read-receipt-setting')) {
|
||||
if (!storage.get('read-receipt-setting') || !reader) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2420,11 +2554,12 @@
|
|||
function onReadSync(ev) {
|
||||
const readAt = ev.timestamp;
|
||||
const { timestamp } = ev.read;
|
||||
const { sender } = ev.read;
|
||||
window.log.info('read sync', sender, timestamp);
|
||||
const { sender, senderUuid } = ev.read;
|
||||
window.log.info('read sync', sender, senderUuid, timestamp);
|
||||
|
||||
const receipt = Whisper.ReadSyncs.add({
|
||||
sender,
|
||||
senderUuid,
|
||||
timestamp,
|
||||
read_at: readAt,
|
||||
});
|
||||
|
@ -2437,7 +2572,8 @@
|
|||
}
|
||||
|
||||
async function onVerified(ev) {
|
||||
const number = ev.verified.destination;
|
||||
const e164 = ev.verified.destination;
|
||||
const uuid = ev.verified.destinationUuid;
|
||||
const key = ev.verified.identityKey;
|
||||
let state;
|
||||
|
||||
|
@ -2446,12 +2582,16 @@
|
|||
}
|
||||
|
||||
const c = new Whisper.Conversation({
|
||||
id: number,
|
||||
e164,
|
||||
uuid,
|
||||
type: 'private',
|
||||
});
|
||||
const error = c.validateNumber();
|
||||
const error = c.validate();
|
||||
if (error) {
|
||||
window.log.error(
|
||||
'Invalid verified sync received:',
|
||||
e164,
|
||||
uuid,
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
return;
|
||||
|
@ -2473,13 +2613,14 @@
|
|||
|
||||
window.log.info(
|
||||
'got verified sync for',
|
||||
number,
|
||||
e164,
|
||||
uuid,
|
||||
state,
|
||||
ev.viaContactSync ? 'via contact sync' : ''
|
||||
);
|
||||
|
||||
const contact = await ConversationController.getOrCreateAndWait(
|
||||
number,
|
||||
e164 || uuid,
|
||||
'private'
|
||||
);
|
||||
const options = {
|
||||
|
@ -2499,17 +2640,27 @@
|
|||
|
||||
function onDeliveryReceipt(ev) {
|
||||
const { deliveryReceipt } = ev;
|
||||
const { sourceUuid, source } = deliveryReceipt;
|
||||
const identifier = source || sourceUuid;
|
||||
|
||||
window.log.info(
|
||||
'delivery receipt from',
|
||||
`${deliveryReceipt.source}.${deliveryReceipt.sourceDevice}`,
|
||||
`${identifier}.${deliveryReceipt.sourceDevice}`,
|
||||
deliveryReceipt.timestamp
|
||||
);
|
||||
|
||||
ev.confirm();
|
||||
|
||||
const deliveredTo = ConversationController.getConversationId(identifier);
|
||||
|
||||
if (!deliveredTo) {
|
||||
window.log.info('no conversation for identifier', identifier);
|
||||
return;
|
||||
}
|
||||
|
||||
const receipt = Whisper.DeliveryReceipts.add({
|
||||
timestamp: deliveryReceipt.timestamp,
|
||||
source: deliveryReceipt.source,
|
||||
deliveredTo,
|
||||
});
|
||||
|
||||
// Note: We don't wait for completion here
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global _, Whisper, Backbone, storage */
|
||||
/* global _, Whisper, Backbone, storage, textsecure */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
|
@ -67,8 +67,8 @@
|
|||
dangerouslyCreateAndAdd(attributes) {
|
||||
return conversations.add(attributes);
|
||||
},
|
||||
getOrCreate(id, type) {
|
||||
if (typeof id !== 'string') {
|
||||
getOrCreate(identifier, type) {
|
||||
if (typeof identifier !== 'string') {
|
||||
throw new TypeError("'id' must be a string");
|
||||
}
|
||||
|
||||
|
@ -84,16 +84,41 @@
|
|||
);
|
||||
}
|
||||
|
||||
let conversation = conversations.get(id);
|
||||
let conversation = conversations.get(identifier);
|
||||
if (conversation) {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
conversation = conversations.add({
|
||||
id,
|
||||
type,
|
||||
version: 2,
|
||||
});
|
||||
const id = window.getGuid();
|
||||
|
||||
if (type === 'group') {
|
||||
conversation = conversations.add({
|
||||
id,
|
||||
uuid: null,
|
||||
e164: null,
|
||||
groupId: identifier,
|
||||
type,
|
||||
version: 2,
|
||||
});
|
||||
} else if (window.isValidGuid(identifier)) {
|
||||
conversation = conversations.add({
|
||||
id,
|
||||
uuid: identifier,
|
||||
e164: null,
|
||||
groupId: null,
|
||||
type,
|
||||
version: 2,
|
||||
});
|
||||
} else {
|
||||
conversation = conversations.add({
|
||||
id,
|
||||
uuid: null,
|
||||
e164: identifier,
|
||||
groupId: null,
|
||||
type,
|
||||
version: 2,
|
||||
});
|
||||
}
|
||||
|
||||
const create = async () => {
|
||||
if (!conversation.isValid()) {
|
||||
|
@ -114,7 +139,7 @@
|
|||
} catch (error) {
|
||||
window.log.error(
|
||||
'Conversation save failed! ',
|
||||
id,
|
||||
identifier,
|
||||
type,
|
||||
'Error:',
|
||||
error && error.stack ? error.stack : error
|
||||
|
@ -142,8 +167,22 @@
|
|||
);
|
||||
});
|
||||
},
|
||||
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;
|
||||
},
|
||||
prepareForSend(id, options) {
|
||||
// id is either a group id or an individual user's id
|
||||
// id is any valid conversation identifier
|
||||
const conversation = this.get(id);
|
||||
const sendOptions = conversation
|
||||
? conversation.getSendOptions(options)
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
const receipts = this.filter(
|
||||
receipt =>
|
||||
receipt.get('timestamp') === message.get('sent_at') &&
|
||||
recipients.indexOf(receipt.get('source')) > -1
|
||||
recipients.indexOf(receipt.get('deliveredTo')) > -1
|
||||
);
|
||||
this.remove(receipts);
|
||||
return receipts;
|
||||
|
@ -34,19 +34,23 @@
|
|||
if (messages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const sourceId = ConversationController.getConversationId(source);
|
||||
const message = messages.find(
|
||||
item => !item.isIncoming() && source === item.get('conversationId')
|
||||
item => !item.isIncoming() && sourceId === item.get('conversationId')
|
||||
);
|
||||
if (message) {
|
||||
return MessageController.register(message.id, message);
|
||||
}
|
||||
|
||||
const groups = await window.Signal.Data.getAllGroupsInvolvingId(source, {
|
||||
ConversationCollection: Whisper.ConversationCollection,
|
||||
});
|
||||
const groups = await window.Signal.Data.getAllGroupsInvolvingId(
|
||||
sourceId,
|
||||
{
|
||||
ConversationCollection: Whisper.ConversationCollection,
|
||||
}
|
||||
);
|
||||
|
||||
const ids = groups.pluck('id');
|
||||
ids.push(source);
|
||||
ids.push(sourceId);
|
||||
|
||||
const target = messages.find(
|
||||
item =>
|
||||
|
@ -68,25 +72,25 @@
|
|||
);
|
||||
|
||||
const message = await this.getTargetMessage(
|
||||
receipt.get('source'),
|
||||
receipt.get('deliveredTo'),
|
||||
messages
|
||||
);
|
||||
if (!message) {
|
||||
window.log.info(
|
||||
'No message for delivery receipt',
|
||||
receipt.get('source'),
|
||||
receipt.get('deliveredTo'),
|
||||
receipt.get('timestamp')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const deliveries = message.get('delivered') || 0;
|
||||
const deliveredTo = message.get('delivered_to') || [];
|
||||
const deliveredTo = message.get('deliveredTo') || [];
|
||||
const expirationStartTimestamp = message.get(
|
||||
'expirationStartTimestamp'
|
||||
);
|
||||
message.set({
|
||||
delivered_to: _.union(deliveredTo, [receipt.get('source')]),
|
||||
delivered_to: _.union(deliveredTo, [receipt.get('deliveredTo')]),
|
||||
delivered: deliveries + 1,
|
||||
expirationStartTimestamp: expirationStartTimestamp || Date.now(),
|
||||
sent: true,
|
||||
|
|
|
@ -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, []);
|
||||
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1118,8 +1118,14 @@ async function importConversations(dir, options) {
|
|||
|
||||
function getMessageKey(message) {
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const ourUuid = textsecure.storage.user.getUuid();
|
||||
const source = message.source || ourNumber;
|
||||
if (source === ourNumber) {
|
||||
const sourceUuid = message.sourceUuid || ourUuid;
|
||||
|
||||
if (
|
||||
(source && source === ourNumber) ||
|
||||
(sourceUuid && sourceUuid === ourUuid)
|
||||
) {
|
||||
return `${source} ${message.timestamp}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global window, setTimeout, IDBKeyRange */
|
||||
/* global window, setTimeout, IDBKeyRange, ConversationController */
|
||||
|
||||
const electron = require('electron');
|
||||
|
||||
|
@ -84,10 +84,10 @@ module.exports = {
|
|||
createOrUpdateSession,
|
||||
createOrUpdateSessions,
|
||||
getSessionById,
|
||||
getSessionsByNumber,
|
||||
getSessionsById,
|
||||
bulkAddSessions,
|
||||
removeSessionById,
|
||||
removeSessionsByNumber,
|
||||
removeSessionsById,
|
||||
removeAllSessions,
|
||||
getAllSessions,
|
||||
|
||||
|
@ -431,10 +431,14 @@ async function removeIndexedDBFiles() {
|
|||
|
||||
const IDENTITY_KEY_KEYS = ['publicKey'];
|
||||
async function createOrUpdateIdentityKey(data) {
|
||||
const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, data);
|
||||
const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, {
|
||||
...data,
|
||||
id: ConversationController.getConversationId(data.id),
|
||||
});
|
||||
await channels.createOrUpdateIdentityKey(updated);
|
||||
}
|
||||
async function getIdentityKeyById(id) {
|
||||
async function getIdentityKeyById(identifier) {
|
||||
const id = ConversationController.getConversationId(identifier);
|
||||
const data = await channels.getIdentityKeyById(id);
|
||||
return keysToArrayBuffer(IDENTITY_KEY_KEYS, data);
|
||||
}
|
||||
|
@ -444,7 +448,8 @@ async function bulkAddIdentityKeys(array) {
|
|||
);
|
||||
await channels.bulkAddIdentityKeys(updated);
|
||||
}
|
||||
async function removeIdentityKeyById(id) {
|
||||
async function removeIdentityKeyById(identifier) {
|
||||
const id = ConversationController.getConversationId(identifier);
|
||||
await channels.removeIdentityKeyById(id);
|
||||
}
|
||||
async function removeAllIdentityKeys() {
|
||||
|
@ -515,6 +520,11 @@ const ITEM_KEYS = {
|
|||
'value.signature',
|
||||
'value.serialized',
|
||||
],
|
||||
senderCertificateWithUuid: [
|
||||
'value.certificate',
|
||||
'value.signature',
|
||||
'value.serialized',
|
||||
],
|
||||
signaling_key: ['value'],
|
||||
profileKey: ['value'],
|
||||
};
|
||||
|
@ -572,8 +582,8 @@ async function getSessionById(id) {
|
|||
const session = await channels.getSessionById(id);
|
||||
return session;
|
||||
}
|
||||
async function getSessionsByNumber(number) {
|
||||
const sessions = await channels.getSessionsByNumber(number);
|
||||
async function getSessionsById(id) {
|
||||
const sessions = await channels.getSessionsById(id);
|
||||
return sessions;
|
||||
}
|
||||
async function bulkAddSessions(array) {
|
||||
|
@ -582,8 +592,8 @@ async function bulkAddSessions(array) {
|
|||
async function removeSessionById(id) {
|
||||
await channels.removeSessionById(id);
|
||||
}
|
||||
async function removeSessionsByNumber(number) {
|
||||
await channels.removeSessionsByNumber(number);
|
||||
async function removeSessionsById(id) {
|
||||
await channels.removeSessionsById(id);
|
||||
}
|
||||
async function removeAllSessions(id) {
|
||||
await channels.removeAllSessions(id);
|
||||
|
@ -799,11 +809,12 @@ async function getAllMessageIds() {
|
|||
|
||||
async function getMessageBySender(
|
||||
// eslint-disable-next-line camelcase
|
||||
{ source, sourceDevice, sent_at },
|
||||
{ source, sourceUuid, sourceDevice, sent_at },
|
||||
{ Message }
|
||||
) {
|
||||
const messages = await channels.getMessageBySender({
|
||||
source,
|
||||
sourceUuid,
|
||||
sourceDevice,
|
||||
sent_at,
|
||||
});
|
||||
|
|
|
@ -117,13 +117,14 @@ function _createSenderCertificateFromBuffer(serialized) {
|
|||
!certificate.identityKey ||
|
||||
!certificate.senderDevice ||
|
||||
!certificate.expires ||
|
||||
!certificate.sender
|
||||
!(certificate.sender || certificate.senderUuid)
|
||||
) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
return {
|
||||
sender: certificate.sender,
|
||||
senderUuid: certificate.senderUuid,
|
||||
senderDevice: certificate.senderDevice,
|
||||
expires: certificate.expires.toNumber(),
|
||||
identityKey: certificate.identityKey.toArrayBuffer(),
|
||||
|
@ -344,7 +345,7 @@ SecretSessionCipher.prototype = {
|
|||
|
||||
// public Pair<SignalProtocolAddress, byte[]> decrypt(
|
||||
// CertificateValidator validator, byte[] ciphertext, long timestamp)
|
||||
async decrypt(validator, ciphertext, timestamp, me) {
|
||||
async decrypt(validator, ciphertext, timestamp, me = {}) {
|
||||
// Capture this.xxx variables to replicate Java's implicit this syntax
|
||||
const signalProtocolStore = this.storage;
|
||||
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
|
||||
|
@ -401,18 +402,29 @@ SecretSessionCipher.prototype = {
|
|||
);
|
||||
}
|
||||
|
||||
const { sender, senderDevice } = content.senderCertificate;
|
||||
const { number, deviceId } = me || {};
|
||||
if (sender === number && senderDevice === deviceId) {
|
||||
const { sender, senderUuid, senderDevice } = content.senderCertificate;
|
||||
if (
|
||||
((sender && me.number && sender === me.number) ||
|
||||
(senderUuid && me.uuid && senderUuid === me.uuid)) &&
|
||||
senderDevice === me.deviceId
|
||||
) {
|
||||
return {
|
||||
isMe: true,
|
||||
};
|
||||
}
|
||||
const address = new libsignal.SignalProtocolAddress(sender, senderDevice);
|
||||
const addressE164 =
|
||||
sender && new libsignal.SignalProtocolAddress(sender, senderDevice);
|
||||
const addressUuid =
|
||||
senderUuid &&
|
||||
new libsignal.SignalProtocolAddress(
|
||||
senderUuid.toLowerCase(),
|
||||
senderDevice
|
||||
);
|
||||
|
||||
try {
|
||||
return {
|
||||
sender: address,
|
||||
sender: addressE164,
|
||||
senderUuid: addressUuid,
|
||||
content: await _decryptWithUnidentifiedSenderMessage(content),
|
||||
};
|
||||
} catch (error) {
|
||||
|
@ -421,7 +433,8 @@ SecretSessionCipher.prototype = {
|
|||
error = new Error('Decryption error was falsey!');
|
||||
}
|
||||
|
||||
error.sender = address;
|
||||
error.sender = addressE164;
|
||||
error.senderUuid = addressUuid;
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
@ -504,7 +517,7 @@ SecretSessionCipher.prototype = {
|
|||
const signalProtocolStore = this.storage;
|
||||
|
||||
const sender = new libsignal.SignalProtocolAddress(
|
||||
message.senderCertificate.sender,
|
||||
message.senderCertificate.sender || message.senderCertificate.senderUuid,
|
||||
message.senderCertificate.senderDevice
|
||||
);
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ const { escapeRegExp } = require('lodash');
|
|||
|
||||
const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..');
|
||||
const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g;
|
||||
const UUID_PATTERN = /[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{10}([0-9A-F]{2})/gi;
|
||||
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
|
||||
const REDACTION_PLACEHOLDER = '[REDACTED]';
|
||||
|
||||
|
@ -64,6 +65,15 @@ exports.redactPhoneNumbers = text => {
|
|||
return text.replace(PHONE_NUMBER_PATTERN, `+${REDACTION_PLACEHOLDER}$1`);
|
||||
};
|
||||
|
||||
// redactUuids :: String -> String
|
||||
exports.redactUuids = text => {
|
||||
if (!is.string(text)) {
|
||||
throw new TypeError("'text' must be a string");
|
||||
}
|
||||
|
||||
return text.replace(UUID_PATTERN, `${REDACTION_PLACEHOLDER}$1`);
|
||||
};
|
||||
|
||||
// redactGroupIds :: String -> String
|
||||
exports.redactGroupIds = text => {
|
||||
if (!is.string(text)) {
|
||||
|
@ -84,7 +94,8 @@ exports.redactSensitivePaths = exports._redactPath(APP_ROOT_PATH);
|
|||
exports.redactAll = compose(
|
||||
exports.redactSensitivePaths,
|
||||
exports.redactGroupIds,
|
||||
exports.redactPhoneNumbers
|
||||
exports.redactPhoneNumbers,
|
||||
exports.redactUuids
|
||||
);
|
||||
|
||||
const removeNewlines = text => text.replace(/\r?\n|\r/g, '');
|
||||
|
|
|
@ -16,7 +16,15 @@ let scheduleNext = null;
|
|||
function refreshOurProfile() {
|
||||
window.log.info('refreshOurProfile');
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const conversation = ConversationController.getOrCreate(ourNumber, 'private');
|
||||
const ourUuid = textsecure.storage.user.getUuid();
|
||||
const conversation = ConversationController.getOrCreate(
|
||||
// This is explicitly ourNumber first in order to avoid creating new
|
||||
// conversations when an old one exists
|
||||
ourNumber || ourUuid,
|
||||
'private'
|
||||
);
|
||||
conversation.updateUuid(ourUuid);
|
||||
conversation.updateE164(ourNumber);
|
||||
conversation.getProfiles();
|
||||
}
|
||||
|
||||
|
@ -66,21 +74,36 @@ function initialize({ events, storage, navigator, logger }) {
|
|||
async function run() {
|
||||
logger.info('refreshSenderCertificate: Getting new certificate...');
|
||||
try {
|
||||
const username = storage.get('number_id');
|
||||
const password = storage.get('password');
|
||||
const server = WebAPI.connect({ username, password });
|
||||
const OLD_USERNAME = storage.get('number_id');
|
||||
const USERNAME = storage.get('uuid_id');
|
||||
const PASSWORD = storage.get('password');
|
||||
const server = WebAPI.connect({
|
||||
username: USERNAME || OLD_USERNAME,
|
||||
password: PASSWORD,
|
||||
});
|
||||
|
||||
const { certificate } = await server.getSenderCertificate();
|
||||
const arrayBuffer = window.Signal.Crypto.base64ToArrayBuffer(certificate);
|
||||
const decoded = textsecure.protobuf.SenderCertificate.decode(arrayBuffer);
|
||||
await Promise.all(
|
||||
[false, true].map(async withUuid => {
|
||||
const { certificate } = await server.getSenderCertificate(withUuid);
|
||||
const arrayBuffer = window.Signal.Crypto.base64ToArrayBuffer(
|
||||
certificate
|
||||
);
|
||||
const decoded = textsecure.protobuf.SenderCertificate.decode(
|
||||
arrayBuffer
|
||||
);
|
||||
|
||||
decoded.certificate = decoded.certificate.toArrayBuffer();
|
||||
decoded.signature = decoded.signature.toArrayBuffer();
|
||||
decoded.serialized = arrayBuffer;
|
||||
decoded.certificate = decoded.certificate.toArrayBuffer();
|
||||
decoded.signature = decoded.signature.toArrayBuffer();
|
||||
decoded.serialized = arrayBuffer;
|
||||
|
||||
storage.put(
|
||||
`senderCertificate${withUuid ? 'WithUuid' : ''}`,
|
||||
decoded
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
storage.put('senderCertificate', decoded);
|
||||
scheduledTime = null;
|
||||
|
||||
scheduleNextRotation();
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
|
|
|
@ -394,12 +394,14 @@ const URL_CALLS = {
|
|||
attachmentId: 'v2/attachments/form/upload',
|
||||
deliveryCert: 'v1/certificate/delivery',
|
||||
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
|
||||
registerCapabilities: 'v1/devices/capabilities',
|
||||
devices: 'v1/devices',
|
||||
keys: 'v2/keys',
|
||||
messages: 'v1/messages',
|
||||
profile: 'v1/profile',
|
||||
signed: 'v2/keys/signed',
|
||||
getStickerPackUpload: 'v1/sticker/pack/form',
|
||||
whoami: 'v1/accounts/whoami',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
@ -451,8 +453,8 @@ function initialize({
|
|||
getAttachment,
|
||||
getAvatar,
|
||||
getDevices,
|
||||
getKeysForNumber,
|
||||
getKeysForNumberUnauth,
|
||||
getKeysForIdentifier,
|
||||
getKeysForIdentifierUnauth,
|
||||
getMessageSocket,
|
||||
getMyKeys,
|
||||
getProfile,
|
||||
|
@ -463,6 +465,7 @@ function initialize({
|
|||
getStickerPackManifest,
|
||||
makeProxiedRequest,
|
||||
putAttachment,
|
||||
registerCapabilities,
|
||||
putStickers,
|
||||
registerKeys,
|
||||
registerSupportForUnauthenticatedDelivery,
|
||||
|
@ -473,6 +476,7 @@ function initialize({
|
|||
sendMessagesUnauth,
|
||||
setSignedPreKey,
|
||||
updateDeviceName,
|
||||
whoami,
|
||||
};
|
||||
|
||||
function _ajax(param) {
|
||||
|
@ -535,12 +539,21 @@ function initialize({
|
|||
});
|
||||
}
|
||||
|
||||
function getSenderCertificate() {
|
||||
function whoami() {
|
||||
return _ajax({
|
||||
call: 'whoami',
|
||||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
});
|
||||
}
|
||||
|
||||
function getSenderCertificate(withUuid = false) {
|
||||
return _ajax({
|
||||
call: 'deliveryCert',
|
||||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
schema: { certificate: 'string' },
|
||||
urlParameters: withUuid ? '?includeUuid=true' : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -552,19 +565,27 @@ function initialize({
|
|||
});
|
||||
}
|
||||
|
||||
function getProfile(number) {
|
||||
function registerCapabilities(capabilities) {
|
||||
return _ajax({
|
||||
call: 'registerCapabilities',
|
||||
httpType: 'PUT',
|
||||
jsonData: { capabilities },
|
||||
});
|
||||
}
|
||||
|
||||
function getProfile(identifier) {
|
||||
return _ajax({
|
||||
call: 'profile',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${number}`,
|
||||
urlParameters: `/${identifier}`,
|
||||
responseType: 'json',
|
||||
});
|
||||
}
|
||||
function getProfileUnauth(number, { accessKey } = {}) {
|
||||
function getProfileUnauth(identifier, { accessKey } = {}) {
|
||||
return _ajax({
|
||||
call: 'profile',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${number}`,
|
||||
urlParameters: `/${identifier}`,
|
||||
responseType: 'json',
|
||||
unauthenticated: true,
|
||||
accessKey,
|
||||
|
@ -623,17 +644,17 @@ function initialize({
|
|||
let call;
|
||||
let urlPrefix;
|
||||
let schema;
|
||||
let responseType;
|
||||
|
||||
if (deviceName) {
|
||||
jsonData.name = deviceName;
|
||||
call = 'devices';
|
||||
urlPrefix = '/';
|
||||
schema = { deviceId: 'number' };
|
||||
responseType = 'json';
|
||||
} else {
|
||||
call = 'accounts';
|
||||
urlPrefix = '/code/';
|
||||
jsonData.capabilities = {
|
||||
uuid: true,
|
||||
};
|
||||
}
|
||||
|
||||
// We update our saved username and password, since we're creating a new account
|
||||
|
@ -643,14 +664,14 @@ function initialize({
|
|||
const response = await _ajax({
|
||||
call,
|
||||
httpType: 'PUT',
|
||||
responseType: 'json',
|
||||
urlParameters: urlPrefix + code,
|
||||
jsonData,
|
||||
responseType,
|
||||
validateResponse: schema,
|
||||
});
|
||||
|
||||
// From here on out, our username will be our phone number combined with device
|
||||
username = `${number}.${response.deviceId || 1}`;
|
||||
// From here on out, our username will be our UUID or E164 combined with device
|
||||
username = `${response.uuid || number}.${response.deviceId || 1}`;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
@ -768,25 +789,25 @@ function initialize({
|
|||
return res;
|
||||
}
|
||||
|
||||
function getKeysForNumber(number, deviceId = '*') {
|
||||
function getKeysForIdentifier(identifier, deviceId = '*') {
|
||||
return _ajax({
|
||||
call: 'keys',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${number}/${deviceId}`,
|
||||
urlParameters: `/${identifier}/${deviceId}`,
|
||||
responseType: 'json',
|
||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||
}).then(handleKeys);
|
||||
}
|
||||
|
||||
function getKeysForNumberUnauth(
|
||||
number,
|
||||
function getKeysForIdentifierUnauth(
|
||||
identifier,
|
||||
deviceId = '*',
|
||||
{ accessKey } = {}
|
||||
) {
|
||||
return _ajax({
|
||||
call: 'keys',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${number}/${deviceId}`,
|
||||
urlParameters: `/${identifier}/${deviceId}`,
|
||||
responseType: 'json',
|
||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||
unauthenticated: true,
|
||||
|
|
|
@ -36,7 +36,9 @@
|
|||
|
||||
const found = messages.find(
|
||||
item =>
|
||||
item.isIncoming() && item.get('source') === receipt.get('sender')
|
||||
item.isIncoming() &&
|
||||
(item.get('source') === receipt.get('sender') ||
|
||||
item.get('sourceUuid') === receipt.get('senderUuid'))
|
||||
);
|
||||
const notificationForMessage = found
|
||||
? Whisper.Notifications.findWhere({ messageId: found.id })
|
||||
|
@ -47,6 +49,7 @@
|
|||
window.log.info(
|
||||
'No message for read sync',
|
||||
receipt.get('sender'),
|
||||
receipt.get('senderUuid'),
|
||||
receipt.get('timestamp')
|
||||
);
|
||||
return;
|
||||
|
|
|
@ -147,6 +147,24 @@
|
|||
},
|
||||
});
|
||||
|
||||
async function normalizeEncodedAddress(encodedAddress) {
|
||||
const [identifier, deviceId] = textsecure.utils.unencodeNumber(
|
||||
encodedAddress
|
||||
);
|
||||
try {
|
||||
const conv = await ConversationController.getOrCreateAndWait(
|
||||
identifier,
|
||||
'private'
|
||||
);
|
||||
return `${conv.get('id')}.${deviceId}`;
|
||||
} catch (e) {
|
||||
window.log.error(
|
||||
`could not get conversation for identifier ${identifier}`
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function SignalProtocolStore() {
|
||||
this.sessionUpdateBatcher = window.Signal.Util.createBatcher({
|
||||
wait: 500,
|
||||
|
@ -322,66 +340,98 @@
|
|||
|
||||
// Sessions
|
||||
|
||||
async loadSession(encodedNumber) {
|
||||
if (encodedNumber === null || encodedNumber === undefined) {
|
||||
async loadSession(encodedAddress) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to get session for undefined/null number');
|
||||
}
|
||||
|
||||
const session = this.sessions[encodedNumber];
|
||||
if (session) {
|
||||
return session.record;
|
||||
try {
|
||||
const id = await normalizeEncodedAddress(encodedAddress);
|
||||
const session = this.sessions[id];
|
||||
|
||||
if (session) {
|
||||
return session.record;
|
||||
}
|
||||
} catch (e) {
|
||||
window.log.error(`could not load session ${encodedAddress}`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
async storeSession(encodedNumber, record) {
|
||||
if (encodedNumber === null || encodedNumber === undefined) {
|
||||
async storeSession(encodedAddress, record) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to put session for undefined/null number');
|
||||
}
|
||||
const unencoded = textsecure.utils.unencodeNumber(encodedNumber);
|
||||
const number = unencoded[0];
|
||||
const unencoded = textsecure.utils.unencodeNumber(encodedAddress);
|
||||
const deviceId = parseInt(unencoded[1], 10);
|
||||
|
||||
const data = {
|
||||
id: encodedNumber,
|
||||
number,
|
||||
deviceId,
|
||||
record,
|
||||
};
|
||||
try {
|
||||
const id = await normalizeEncodedAddress(encodedAddress);
|
||||
|
||||
this.sessions[encodedNumber] = data;
|
||||
const data = {
|
||||
id,
|
||||
conversationId: textsecure.utils.unencodeNumber(id)[0],
|
||||
deviceId,
|
||||
record,
|
||||
};
|
||||
|
||||
// Note: Because these are cached in memory, we batch and make these database
|
||||
// updates out of band.
|
||||
this.sessionUpdateBatcher.add(data);
|
||||
this.sessions[id] = data;
|
||||
|
||||
// Note: Because these are cached in memory, we batch and make these database
|
||||
// updates out of band.
|
||||
this.sessionUpdateBatcher.add(data);
|
||||
} catch (e) {
|
||||
window.log.error(`could not store session for ${encodedAddress}`);
|
||||
}
|
||||
},
|
||||
async getDeviceIds(number) {
|
||||
if (number === null || number === undefined) {
|
||||
async getDeviceIds(identifier) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to get device ids for undefined/null number');
|
||||
}
|
||||
|
||||
const allSessions = Object.values(this.sessions);
|
||||
const sessions = allSessions.filter(session => session.number === number);
|
||||
return _.pluck(sessions, 'deviceId');
|
||||
try {
|
||||
const id = ConversationController.getConversationId(identifier);
|
||||
const allSessions = Object.values(this.sessions);
|
||||
const sessions = allSessions.filter(
|
||||
session => session.conversationId === id
|
||||
);
|
||||
|
||||
return _.pluck(sessions, 'deviceId');
|
||||
} catch (e) {
|
||||
window.log.error(
|
||||
`could not get device ids for identifier ${identifier}`
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
async removeSession(encodedNumber) {
|
||||
window.log.info('deleting session for ', encodedNumber);
|
||||
delete this.sessions[encodedNumber];
|
||||
await window.Signal.Data.removeSessionById(encodedNumber);
|
||||
async removeSession(encodedAddress) {
|
||||
window.log.info('deleting session for ', encodedAddress);
|
||||
try {
|
||||
const id = await normalizeEncodedAddress(encodedAddress);
|
||||
delete this.sessions[id];
|
||||
await window.Signal.Data.removeSessionById(id);
|
||||
} catch (e) {
|
||||
window.log.error(`could not delete session for ${encodedAddress}`);
|
||||
}
|
||||
},
|
||||
async removeAllSessions(number) {
|
||||
if (number === null || number === undefined) {
|
||||
async removeAllSessions(identifier) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to remove sessions for undefined/null number');
|
||||
}
|
||||
|
||||
const id = ConversationController.getConversationId(identifier);
|
||||
|
||||
const allSessions = Object.values(this.sessions);
|
||||
|
||||
for (let i = 0, max = allSessions.length; i < max; i += 1) {
|
||||
const session = allSessions[i];
|
||||
if (session.number === number) {
|
||||
if (session.conversationId === id) {
|
||||
delete this.sessions[session.id];
|
||||
}
|
||||
}
|
||||
await window.Signal.Data.removeSessionsByNumber(number);
|
||||
|
||||
await window.Signal.Data.removeSessionsById(identifier);
|
||||
},
|
||||
async archiveSiblingSessions(identifier) {
|
||||
const address = libsignal.SignalProtocolAddress.fromString(identifier);
|
||||
|
@ -404,12 +454,15 @@
|
|||
})
|
||||
);
|
||||
},
|
||||
async archiveAllSessions(number) {
|
||||
const deviceIds = await this.getDeviceIds(number);
|
||||
async archiveAllSessions(identifier) {
|
||||
const deviceIds = await this.getDeviceIds(identifier);
|
||||
|
||||
await Promise.all(
|
||||
deviceIds.map(async deviceId => {
|
||||
const address = new libsignal.SignalProtocolAddress(number, deviceId);
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
deviceId
|
||||
);
|
||||
window.log.info('closing session for', address.toString());
|
||||
const sessionCipher = new libsignal.SessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
|
@ -426,16 +479,35 @@
|
|||
|
||||
// Identity Keys
|
||||
|
||||
async isTrustedIdentity(identifier, publicKey, direction) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
getIdentityRecord(identifier) {
|
||||
try {
|
||||
const id = ConversationController.getConversationId(identifier);
|
||||
const record = this.identityKeys[id];
|
||||
|
||||
if (record) {
|
||||
return record;
|
||||
}
|
||||
} catch (e) {
|
||||
window.log.error(
|
||||
`could not get identity record for identifier ${identifier}`
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
async isTrustedIdentity(encodedAddress, publicKey, direction) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to get identity key for undefined/null key');
|
||||
}
|
||||
const number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const isOurNumber = number === textsecure.storage.user.getNumber();
|
||||
const identifier = textsecure.utils.unencodeNumber(encodedAddress)[0];
|
||||
const isOurIdentifier =
|
||||
identifier === textsecure.storage.user.getNumber() ||
|
||||
identifier === textsecure.storage.user.getUuid();
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
|
||||
if (isOurNumber) {
|
||||
if (isOurIdentifier) {
|
||||
const existing = identityRecord ? identityRecord.publicKey : null;
|
||||
return equalArrayBuffers(existing, publicKey);
|
||||
}
|
||||
|
@ -482,8 +554,8 @@
|
|||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to get identity key for undefined/null key');
|
||||
}
|
||||
const number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const id = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const identityRecord = this.getIdentityRecord(id);
|
||||
|
||||
if (identityRecord) {
|
||||
return identityRecord.publicKey;
|
||||
|
@ -496,8 +568,8 @@
|
|||
this.identityKeys[id] = data;
|
||||
await window.Signal.Data.createOrUpdateIdentityKey(data);
|
||||
},
|
||||
async saveIdentity(identifier, publicKey, nonblockingApproval) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
async saveIdentity(encodedAddress, publicKey, nonblockingApproval) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to put identity key for undefined/null key');
|
||||
}
|
||||
if (!(publicKey instanceof ArrayBuffer)) {
|
||||
|
@ -509,14 +581,15 @@
|
|||
nonblockingApproval = false;
|
||||
}
|
||||
|
||||
const number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identifer = textsecure.utils.unencodeNumber(encodedAddress)[0];
|
||||
const identityRecord = this.getIdentityRecord(identifer);
|
||||
const id = ConversationController.getConversationId(identifer);
|
||||
|
||||
if (!identityRecord || !identityRecord.publicKey) {
|
||||
// Lookup failed, or the current key was removed, so save this one.
|
||||
window.log.info('Saving new identity...');
|
||||
await this._saveIdentityKey({
|
||||
id: number,
|
||||
id,
|
||||
publicKey,
|
||||
firstUse: true,
|
||||
timestamp: Date.now(),
|
||||
|
@ -542,7 +615,7 @@
|
|||
}
|
||||
|
||||
await this._saveIdentityKey({
|
||||
id: number,
|
||||
id,
|
||||
publicKey,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
|
@ -551,14 +624,14 @@
|
|||
});
|
||||
|
||||
try {
|
||||
this.trigger('keychange', number);
|
||||
this.trigger('keychange', identifer);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'saveIdentity error triggering keychange:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
await this.archiveSiblingSessions(identifier);
|
||||
await this.archiveSiblingSessions(encodedAddress);
|
||||
|
||||
return true;
|
||||
} else if (this.isNonBlockingApprovalRequired(identityRecord)) {
|
||||
|
@ -579,16 +652,21 @@
|
|||
!identityRecord.nonblockingApproval
|
||||
);
|
||||
},
|
||||
async saveIdentityWithAttributes(identifier, attributes) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
async saveIdentityWithAttributes(encodedAddress, attributes) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to put identity key for undefined/null key');
|
||||
}
|
||||
|
||||
const number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identifier = textsecure.utils.unencodeNumber(encodedAddress)[0];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
const conv = await ConversationController.getOrCreateAndWait(
|
||||
identifier,
|
||||
'private'
|
||||
);
|
||||
const id = conv.get('id');
|
||||
|
||||
const updates = {
|
||||
id: number,
|
||||
id,
|
||||
...identityRecord,
|
||||
...attributes,
|
||||
};
|
||||
|
@ -600,26 +678,26 @@
|
|||
throw model.validationError;
|
||||
}
|
||||
},
|
||||
async setApproval(identifier, nonblockingApproval) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
async setApproval(encodedAddress, nonblockingApproval) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to set approval for undefined/null identifier');
|
||||
}
|
||||
if (typeof nonblockingApproval !== 'boolean') {
|
||||
throw new Error('Invalid approval status');
|
||||
}
|
||||
|
||||
const number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identifier = textsecure.utils.unencodeNumber(encodedAddress)[0];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
|
||||
if (!identityRecord) {
|
||||
throw new Error(`No identity record for ${number}`);
|
||||
throw new Error(`No identity record for ${identifier}`);
|
||||
}
|
||||
|
||||
identityRecord.nonblockingApproval = nonblockingApproval;
|
||||
await this._saveIdentityKey(identityRecord);
|
||||
},
|
||||
async setVerified(number, verifiedStatus, publicKey) {
|
||||
if (number === null || number === undefined) {
|
||||
async setVerified(encodedAddress, verifiedStatus, publicKey) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
if (!validateVerifiedStatus(verifiedStatus)) {
|
||||
|
@ -629,9 +707,10 @@
|
|||
throw new Error('Invalid public key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identityRecord = this.getIdentityRecord(encodedAddress);
|
||||
|
||||
if (!identityRecord) {
|
||||
throw new Error(`No identity record for ${number}`);
|
||||
throw new Error(`No identity record for ${encodedAddress}`);
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -650,14 +729,14 @@
|
|||
window.log.info('No identity record for specified publicKey');
|
||||
}
|
||||
},
|
||||
async getVerified(number) {
|
||||
if (number === null || number === undefined) {
|
||||
async getVerified(identifier) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
if (!identityRecord) {
|
||||
throw new Error(`No identity record for ${number}`);
|
||||
throw new Error(`No identity record for ${identifier}`);
|
||||
}
|
||||
|
||||
const verifiedStatus = identityRecord.verified;
|
||||
|
@ -681,15 +760,16 @@
|
|||
// This function encapsulates the non-Java behavior, since the mobile apps don't
|
||||
// currently receive contact syncs and therefore will see a verify sync with
|
||||
// UNVERIFIED status
|
||||
async processUnverifiedMessage(number, verifiedStatus, publicKey) {
|
||||
if (number === null || number === undefined) {
|
||||
async processUnverifiedMessage(identifier, verifiedStatus, publicKey) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) {
|
||||
throw new Error('Invalid public key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
|
||||
const isPresent = Boolean(identityRecord);
|
||||
let isEqual = false;
|
||||
|
||||
|
@ -703,7 +783,7 @@
|
|||
identityRecord.verified !== VerifiedStatus.UNVERIFIED
|
||||
) {
|
||||
await textsecure.storage.protocol.setVerified(
|
||||
number,
|
||||
identifier,
|
||||
verifiedStatus,
|
||||
publicKey
|
||||
);
|
||||
|
@ -711,17 +791,20 @@
|
|||
}
|
||||
|
||||
if (!isPresent || !isEqual) {
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(number, {
|
||||
publicKey,
|
||||
verified: verifiedStatus,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
nonblockingApproval: true,
|
||||
});
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(
|
||||
identifier,
|
||||
{
|
||||
publicKey,
|
||||
verified: verifiedStatus,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
nonblockingApproval: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (isPresent && !isEqual) {
|
||||
try {
|
||||
this.trigger('keychange', number);
|
||||
this.trigger('keychange', identifier);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'processUnverifiedMessage error triggering keychange:',
|
||||
|
@ -729,7 +812,7 @@
|
|||
);
|
||||
}
|
||||
|
||||
await this.archiveAllSessions(number);
|
||||
await this.archiveAllSessions(identifier);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -743,8 +826,8 @@
|
|||
},
|
||||
// This matches the Java method as of
|
||||
// https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188
|
||||
async processVerifiedMessage(number, verifiedStatus, publicKey) {
|
||||
if (number === null || number === undefined) {
|
||||
async processVerifiedMessage(identifier, verifiedStatus, publicKey) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
if (!validateVerifiedStatus(verifiedStatus)) {
|
||||
|
@ -754,7 +837,7 @@
|
|||
throw new Error('Invalid public key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
|
||||
const isPresent = Boolean(identityRecord);
|
||||
let isEqual = false;
|
||||
|
@ -775,7 +858,7 @@
|
|||
verifiedStatus === VerifiedStatus.DEFAULT
|
||||
) {
|
||||
await textsecure.storage.protocol.setVerified(
|
||||
number,
|
||||
identifier,
|
||||
verifiedStatus,
|
||||
publicKey
|
||||
);
|
||||
|
@ -788,17 +871,20 @@
|
|||
(isPresent && !isEqual) ||
|
||||
(isPresent && identityRecord.verified !== VerifiedStatus.VERIFIED))
|
||||
) {
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(number, {
|
||||
publicKey,
|
||||
verified: verifiedStatus,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
nonblockingApproval: true,
|
||||
});
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(
|
||||
identifier,
|
||||
{
|
||||
publicKey,
|
||||
verified: verifiedStatus,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
nonblockingApproval: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (isPresent && !isEqual) {
|
||||
try {
|
||||
this.trigger('keychange', number);
|
||||
this.trigger('keychange', identifier);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'processVerifiedMessage error triggering keychange:',
|
||||
|
@ -806,7 +892,7 @@
|
|||
);
|
||||
}
|
||||
|
||||
await this.archiveAllSessions(number);
|
||||
await this.archiveAllSessions(identifier);
|
||||
|
||||
// true signifies that we overwrote a previous key with a new one
|
||||
return true;
|
||||
|
@ -818,14 +904,14 @@
|
|||
// state we had before.
|
||||
return false;
|
||||
},
|
||||
async isUntrusted(number) {
|
||||
if (number === null || number === undefined) {
|
||||
async isUntrusted(identifier) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
if (!identityRecord) {
|
||||
throw new Error(`No identity record for ${number}`);
|
||||
throw new Error(`No identity record for ${identifier}`);
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -838,10 +924,13 @@
|
|||
|
||||
return false;
|
||||
},
|
||||
async removeIdentityKey(number) {
|
||||
delete this.identityKeys[number];
|
||||
await window.Signal.Data.removeIdentityKeyById(number);
|
||||
await textsecure.storage.protocol.removeAllSessions(number);
|
||||
async removeIdentityKey(identifier) {
|
||||
const id = ConversationController.getConversationId(identifier);
|
||||
if (id) {
|
||||
delete this.identityKeys[id];
|
||||
await window.Signal.Data.removeIdentityKeyById(id);
|
||||
await textsecure.storage.protocol.removeAllSessions(id);
|
||||
}
|
||||
},
|
||||
|
||||
// Not yet processed messages - for resiliency
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
Whisper.ViewSyncs = new (Backbone.Collection.extend({
|
||||
forMessage(message) {
|
||||
const sync = this.findWhere({
|
||||
source: message.get('source'),
|
||||
conversationId: message.get('conversationId'),
|
||||
timestamp: message.get('sent_at'),
|
||||
});
|
||||
if (sync) {
|
||||
|
@ -35,13 +35,15 @@
|
|||
);
|
||||
|
||||
const found = messages.find(
|
||||
item => item.get('source') === sync.get('source')
|
||||
item => item.get('conversationId') === sync.get('conversationId')
|
||||
);
|
||||
const syncSource = sync.get('source');
|
||||
const syncSourceUuid = sync.get('sourceUuid');
|
||||
const syncTimestamp = sync.get('timestamp');
|
||||
const wasMessageFound = Boolean(found);
|
||||
window.log.info('Receive view sync:', {
|
||||
syncSource,
|
||||
syncSourceUuid,
|
||||
syncTimestamp,
|
||||
wasMessageFound,
|
||||
});
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
this.contactView = null;
|
||||
}
|
||||
|
||||
const isMe = this.ourNumber === this.model.id;
|
||||
const isMe = this.model.isMe();
|
||||
|
||||
this.contactView = new Whisper.ReactWrapperView({
|
||||
className: 'contact-wrapper',
|
||||
|
@ -47,7 +47,7 @@
|
|||
return this;
|
||||
},
|
||||
showIdentity() {
|
||||
if (this.model.id === this.ourNumber || this.loading) {
|
||||
if (this.model.isMe() || this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -2004,7 +2004,7 @@
|
|||
await contact.setApproved();
|
||||
}
|
||||
|
||||
message.resend(contact.id);
|
||||
message.resend(contact.get('e164'), contact.get('uuid'));
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -2483,6 +2483,7 @@
|
|||
try {
|
||||
await this.model.sendReactionMessage(reaction, {
|
||||
targetAuthorE164: messageModel.getSource(),
|
||||
targetAuthorUuid: messageModel.getSourceUuid(),
|
||||
targetTimestamp: messageModel.get('sent_at'),
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -2668,10 +2669,17 @@
|
|||
if (window.reduxStore.getState().expiration.hasExpired) {
|
||||
ToastView = Whisper.ExpiredToast;
|
||||
}
|
||||
if (this.model.isPrivate() && storage.isBlocked(this.model.id)) {
|
||||
if (
|
||||
this.model.isPrivate() &&
|
||||
(storage.isBlocked(this.model.get('e164')) ||
|
||||
storage.isUuidBlocked(this.model.get('uuid')))
|
||||
) {
|
||||
ToastView = Whisper.BlockedToast;
|
||||
}
|
||||
if (!this.model.isPrivate() && storage.isGroupBlocked(this.model.id)) {
|
||||
if (
|
||||
!this.model.isPrivate() &&
|
||||
storage.isGroupBlocked(this.model.get('groupId'))
|
||||
) {
|
||||
ToastView = Whisper.BlockedGroupToast;
|
||||
}
|
||||
if (!this.model.isPrivate() && this.model.get('left')) {
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
},
|
||||
initialize(options) {
|
||||
this.ourNumber = textsecure.storage.user.getNumber();
|
||||
this.ourUuid = textsecure.storage.user.getUuid();
|
||||
if (options.newKey) {
|
||||
this.theirKey = options.newKey;
|
||||
}
|
||||
|
@ -44,16 +45,29 @@
|
|||
);
|
||||
},
|
||||
loadTheirKey() {
|
||||
const item = textsecure.storage.protocol.identityKeys[this.model.id];
|
||||
const item = textsecure.storage.protocol.getIdentityRecord(
|
||||
this.model.get('id')
|
||||
);
|
||||
this.theirKey = item ? item.publicKey : null;
|
||||
},
|
||||
loadOurKey() {
|
||||
const item = textsecure.storage.protocol.identityKeys[this.ourNumber];
|
||||
const item = textsecure.storage.protocol.getIdentityRecord(
|
||||
this.ourUuid || this.ourNumber
|
||||
);
|
||||
this.ourKey = item ? item.publicKey : null;
|
||||
},
|
||||
generateSecurityNumber() {
|
||||
return new libsignal.FingerprintGenerator(5200)
|
||||
.createFor(this.ourNumber, this.ourKey, this.model.id, this.theirKey)
|
||||
.createFor(
|
||||
// TODO: we cannot use UUIDs for safety numbers yet
|
||||
// this.ourUuid || this.ourNumber,
|
||||
this.ourNumber,
|
||||
this.ourKey,
|
||||
// TODO: we cannot use UUIDs for safety numbers yet
|
||||
// this.model.get('uuid') || this.model.get('e164'),
|
||||
this.model.get('e164'),
|
||||
this.theirKey
|
||||
)
|
||||
.then(securityNumber => {
|
||||
this.securityNumber = securityNumber;
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue