Migrate to SQLCipher for messages/cache

Quite a few other fixes, including:
  - Sending to contact with no avatar yet (not synced from mobile)
  - Left pane doesn't update quickly or at all on new message
  - Left pane doesn't show sent or error status

Also:
 - Contributing.md: Ensure set of linux dev dependencies is complete
This commit is contained in:
Scott Nonnenberg 2018-07-26 18:13:56 -07:00
parent fc461c82ce
commit 3105b77475
29 changed files with 2006 additions and 716 deletions

View file

@ -22,7 +22,9 @@
const {
deleteExternalMessageFiles,
getAbsoluteAttachmentPath,
} = Signal.Migrations;
loadAttachmentData,
loadQuoteData,
} = window.Signal.Migrations;
window.AccountCache = Object.create(null);
window.AccountJobs = Object.create(null);
@ -54,8 +56,6 @@
window.hasSignalAccount = number => window.AccountCache[number];
window.Whisper.Message = Backbone.Model.extend({
database: Whisper.Database,
storeName: 'messages',
initialize(attributes) {
if (_.isObject(attributes)) {
this.set(
@ -211,9 +211,11 @@
return '';
},
onDestroy() {
this.cleanup();
},
async cleanup() {
this.unload();
return deleteExternalMessageFiles(this.attributes);
await deleteExternalMessageFiles(this.attributes);
},
unload() {
if (this.quotedMessage) {
@ -269,16 +271,16 @@
disabled,
};
if (source === this.OUR_NUMBER) {
return {
...basicProps,
type: 'fromMe',
};
} else if (fromSync) {
if (fromSync) {
return {
...basicProps,
type: 'fromSync',
};
} else if (source === this.OUR_NUMBER) {
return {
...basicProps,
type: 'fromMe',
};
}
return basicProps;
@ -416,7 +418,7 @@
const authorColor = contactModel ? contactModel.getColor() : null;
const authorAvatar = contactModel ? contactModel.getAvatar() : null;
const authorAvatarPath = authorAvatar.url;
const authorAvatarPath = authorAvatar ? authorAvatar.url : null;
const expirationLength = this.get('expireTimer') * 1000;
const expireTimerStart = this.get('expirationStartTimestamp');
@ -654,15 +656,117 @@
contacts: sortedContacts,
};
},
retrySend() {
const retries = _.filter(
// One caller today: event handler for the 'Retry Send' entry in triple-dot menu
async retrySend() {
const [retries, errors] = _.partition(
this.get('errors'),
this.isReplayableError.bind(this)
);
_.map(retries, 'number').forEach(number => {
this.resend(number);
});
// Remove the errors that aren't replayable
this.set({ errors });
const profileKey = null;
const numbers = retries.map(retry => retry.number);
if (!numbers.length) {
window.log.error(
'retrySend: Attempted to retry, but no numbers to send to!'
);
return null;
}
const attachmentsWithData = await Promise.all(
(this.get('attachments') || []).map(loadAttachmentData)
);
const quoteWithData = await loadQuoteData(this.get('quote'));
const conversation = this.getConversation();
let promise;
if (conversation.isPrivate()) {
const [number] = numbers;
promise = textsecure.messaging.sendMessageToNumber(
number,
this.get('body'),
attachmentsWithData,
quoteWithData,
this.get('sent_at'),
this.get('expireTimer'),
profileKey
);
} else {
// Because this is a partial group send, we manually construct the request like
// sendMessageToGroup does.
promise = textsecure.messaging.sendMessage({
recipients: numbers,
body: this.get('body'),
timestamp: this.get('sent_at'),
attachments: attachmentsWithData,
quote: quoteWithData,
needsSync: !this.get('synced'),
expireTimer: this.get('expireTimer'),
profileKey,
group: {
id: this.get('conversationId'),
type: textsecure.protobuf.GroupContext.Type.DELIVER,
},
});
}
return this.send(promise);
},
isReplayableError(e) {
return (
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError'
);
},
// 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);
if (error) {
const profileKey = null;
const attachmentsWithData = await Promise.all(
(this.get('attachments') || []).map(loadAttachmentData)
);
const quoteWithData = await loadQuoteData(this.get('quote'));
const promise = textsecure.messaging.sendMessageToNumber(
number,
this.get('body'),
attachmentsWithData,
quoteWithData,
this.get('sent_at'),
this.get('expireTimer'),
profileKey
);
this.send(promise);
}
},
removeOutgoingErrors(number) {
const errors = _.partition(
this.get('errors'),
e =>
e.number === number &&
(e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError')
);
this.set({ errors: errors[1] });
return errors[0][0];
},
getConversation() {
// This needs to be an unsafe call, because this method is called during
// initial module setup. We may be in the middle of the initial fetch to
@ -720,9 +824,12 @@
.then(async result => {
const now = Date.now();
this.trigger('done');
// This is used by sendSyncMessage, then set to null
if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
}
const sentTo = this.get('sent_to') || [];
this.set({
sent_to: _.union(sentTo, result.successfulNumbers),
@ -739,6 +846,7 @@
.catch(result => {
const now = Date.now();
this.trigger('done');
if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
}
@ -774,9 +882,9 @@
);
}
return Promise.all(promises).then(() => {
this.trigger('send-error', this.get('errors'));
});
this.trigger('send-error', this.get('errors'));
return Promise.all(promises);
});
},
@ -855,7 +963,6 @@
Message: Whisper.Message,
});
},
hasNetworkError() {
const error = _.find(
this.get('errors'),
@ -867,36 +974,6 @@
);
return !!error;
},
removeOutgoingErrors(number) {
const errors = _.partition(
this.get('errors'),
e =>
e.number === number &&
(e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError')
);
this.set({ errors: errors[1] });
return errors[0][0];
},
isReplayableError(e) {
return (
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError'
);
},
resend(number) {
const error = this.removeOutgoingErrors(number);
if (error) {
const promise = new textsecure.ReplayableError(error).replay();
this.send(promise);
}
},
handleDataMessage(dataMessage, confirm) {
// This function is called from the background script in a few scenarios:
// 1. on an incoming message
@ -1217,10 +1294,12 @@
const expiresAt = start + delta;
this.set({ expires_at: expiresAt });
const id = await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
this.set({ id });
const id = this.get('id');
if (id) {
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
}
Whisper.ExpiringMessagesListener.update();
window.log.info('Set message expiration', {
@ -1233,6 +1312,7 @@
Whisper.MessageCollection = Backbone.Collection.extend({
model: Whisper.Message,
// Keeping this for legacy upgrade pre-migrate to SQLCipher
database: Whisper.Database,
storeName: 'messages',
comparator(left, right) {
@ -1282,7 +1362,15 @@
}
);
this.add(messages.models);
const models = messages.filter(message => Boolean(message.id));
const eliminated = messages.length - models.length;
if (eliminated > 0) {
window.log.warn(
`fetchConversation: Eliminated ${eliminated} messages without an id`
);
}
this.add(models);
if (unreadCount <= 0) {
return;