diff --git a/Gruntfile.js b/Gruntfile.js index f307ae46..80e064dc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -57,6 +57,7 @@ module.exports = function(grunt) { 'libtextsecure/api.js', 'libtextsecure/account_manager.js', 'libtextsecure/message_receiver.js', + 'libtextsecure/outgoing_message.js', 'libtextsecure/sendmessage.js', 'libtextsecure/contacts_parser.js', ], diff --git a/js/libtextsecure.js b/js/libtextsecure.js index c13c4b5e..fd0fbd56 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -39629,6 +39629,158 @@ textsecure.MessageReceiver.prototype = { }; +/* + * vim: ts=4:sw=4:expandtab + */ +function OutgoingMessage(timestamp, numbers, message, callback, messageSender) { + this.timestamp = timestamp; + this.numbers = numbers; + this.message = message; + this.sender = messageSender; + this.server = messageSender.server; + this.callback = callback; + + this.numbersCompleted = 0; + this.errors = []; + this.successfulNumbers = []; +} + +OutgoingMessage.prototype = { + constructor: OutgoingMessage, + numberCompleted: function() { + this.numbersCompleted++; + if (this.numbersCompleted >= this.numbers.length) { + this.callback({success: this.successfulNumbers, failure: this.errors}); + } + }, + registerError: function(number, reason, error) { + if (!error || error.name === 'HTTPError') { + error = new textsecure.OutgoingMessageError(number, this.message.toArrayBuffer(), this.timestamp, error); + } + + error.number = number; + error.reason = reason; + this.errors[this.errors.length] = error; + this.numberCompleted(); + }, + reloadDevicesAndSend: function(number, recurse) { + return function() { + return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) { + if (devicesForNumber.length == 0) + return this.registerError(number, "Got empty device list when loading device keys", null); + return this.doSendMessage(number, devicesForNumber, recurse); + }.bind(this)); + }.bind(this); + }, + + getKeysForNumber: function(number, updateDevices) { + var handleResult = function(response) { + return Promise.all(response.devices.map(function(device) { + if (updateDevices === undefined || updateDevices.indexOf(device.deviceId) > -1) + return textsecure.storage.devices.saveKeysToDeviceObject({ + encodedNumber: number + "." + device.deviceId, + identityKey: response.identityKey, + preKey: device.preKey.publicKey, + preKeyId: device.preKey.keyId, + signedKey: device.signedPreKey.publicKey, + signedKeyId: device.signedPreKey.keyId, + signedKeySignature: device.signedPreKey.signature, + registrationId: device.registrationId + }).catch(function(error) { + if (error.message === "Identity key changed") { + error = new textsecure.OutgoingIdentityKeyError(number, this.message.toArrayBuffer(), this.timestamp, error.identityKey); + this.registerError(number, "Identity key changed", error); + } + throw error; + }.bind(this)); + }.bind(this))); + }.bind(this); + + if (updateDevices === undefined) { + return this.server.getKeysForNumber(number).then(handleResult); + } else { + var promises = updateDevices.map(function(device) { + return this.server.getKeysForNumber(number, device).then(handleResult); + }.bind(this)); + + return Promise.all(promises); + } + }, + + doSendMessage: function(number, devicesForNumber, recurse) { + return this.sender.encryptToDevices(this.timestamp, number, devicesForNumber, this.message).then(function(jsonData) { + return this.sender.transmitMessage(number, jsonData).then(function() { + this.successfulNumbers[this.successfulNumbers.length] = number; + this.numberCompleted(); + }.bind(this)); + }.bind(this)).catch(function(error) { + if (error instanceof Error && error.name == "HTTPError" && (error.code == 410 || error.code == 409)) { + if (!recurse) + return this.registerError(number, "Hit retry limit attempting to reload device list", error); + + var p; + if (error.code == 409) { + p = textsecure.storage.devices.removeDeviceIdsForNumber(number, error.response.extraDevices); + } else { + p = Promise.all(error.response.staleDevices.map(function(deviceId) { + return textsecure.protocol_wrapper.closeOpenSessionForDevice(number + '.' + deviceId); + })); + } + + return p.then(function() { + var resetDevices = ((error.code == 410) ? error.response.staleDevices : error.response.missingDevices); + return this.getKeysForNumber(number, resetDevices) + .then(this.reloadDevicesAndSend(number, (error.code == 409))) + .catch(function(error) { + this.registerError(number, "Failed to reload device keys", error); + }.bind(this)); + }.bind(this)); + } else { + this.registerError(number, "Failed to create or send message", error); + } + }.bind(this)); + }, + + sendToNumber: function(number) { + return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) { + return Promise.all(devicesForNumber.map(function(device) { + return textsecure.protocol_wrapper.hasOpenSession(device.encodedNumber).then(function(result) { + if (!result) + return this.getKeysForNumber(number, [parseInt(textsecure.utils.unencodeNumber(device.encodedNumber)[1])]); + }.bind(this)); + }.bind(this))).then(function() { + return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) { + if (devicesForNumber.length == 0) { + return this.getKeysForNumber(number, [1]) + .then(this.reloadDevicesAndSend(number, true)) + .catch(function(error) { + this.registerError(number, "Failed to retreive new device keys for number " + number, error); + }.bind(this)); + } else { + return this.doSendMessage(number, devicesForNumber, true); + } + }.bind(this)); + }.bind(this)); + }.bind(this)); + }, + + send: function() { + this.numbers.forEach(function(number) { + var sendPrevious = this.sender.pendingMessages[number] || Promise.resolve(); + var sendCurrent = this.sender.pendingMessages[number] = sendPrevious.then(function() { + return this.sendToNumber(number); + }.bind(this)).catch(function() { + return this.sendToNumber(number); + }.bind(this)); + sendCurrent.then(function() { + if (this.sender.pendingMessages[number] === sendCurrent) { + delete this.sender.pendingMessages[number]; + } + }.bind(this)); + }.bind(this)); + } +}; + /* * vim: ts=4:sw=4:expandtab */ @@ -39711,140 +39863,8 @@ MessageSender.prototype = { }, sendMessageProto: function(timestamp, numbers, message, callback) { - var numbersCompleted = 0; - var errors = []; - var successfulNumbers = []; - - var numberCompleted = function() { - numbersCompleted++; - if (numbersCompleted >= numbers.length) - callback({success: successfulNumbers, failure: errors}); - }; - - var registerError = function(number, reason, error) { - if (!error || error.name === 'HTTPError') { - error = new textsecure.OutgoingMessageError(number, message.toArrayBuffer(), timestamp, error); - } - - error.number = number; - error.reason = reason; - errors[errors.length] = error; - numberCompleted(); - }; - - var reloadDevicesAndSend = function(number, recurse) { - return function() { - return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) { - if (devicesForNumber.length == 0) - return registerError(number, "Got empty device list when loading device keys", null); - return doSendMessage(number, devicesForNumber, recurse); - }); - } - }; - - var getKeysForNumber = function(number, updateDevices) { - var handleResult = function(response) { - return Promise.all(response.devices.map(function(device) { - if (updateDevices === undefined || updateDevices.indexOf(device.deviceId) > -1) - return textsecure.storage.devices.saveKeysToDeviceObject({ - encodedNumber: number + "." + device.deviceId, - identityKey: response.identityKey, - preKey: device.preKey.publicKey, - preKeyId: device.preKey.keyId, - signedKey: device.signedPreKey.publicKey, - signedKeyId: device.signedPreKey.keyId, - signedKeySignature: device.signedPreKey.signature, - registrationId: device.registrationId - }).catch(function(error) { - if (error.message === "Identity key changed") { - error = new textsecure.OutgoingIdentityKeyError(number, message.toArrayBuffer(), timestamp, error.identityKey); - registerError(number, "Identity key changed", error); - } - throw error; - }); - })); - }; - - if (updateDevices === undefined) { - return this.server.getKeysForNumber(number).then(handleResult); - } else { - var promises = updateDevices.map(function(device) { - return this.server.getKeysForNumber(number, device).then(handleResult); - }.bind(this)); - - return Promise.all(promises); - } - }.bind(this); - - var doSendMessage = function(number, devicesForNumber, recurse) { - return this.encryptToDevices(timestamp, number, devicesForNumber, message).then(function(jsonData) { - return this.transmitMessage(number, jsonData).then(function() { - successfulNumbers[successfulNumbers.length] = number; - numberCompleted(); - }.bind(this)); - }.bind(this)).catch(function(error) { - if (error instanceof Error && error.name == "HTTPError" && (error.code == 410 || error.code == 409)) { - if (!recurse) - return registerError(number, "Hit retry limit attempting to reload device list", error); - - var p; - if (error.code == 409) { - p = textsecure.storage.devices.removeDeviceIdsForNumber(number, error.response.extraDevices); - } else { - p = Promise.all(error.response.staleDevices.map(function(deviceId) { - return textsecure.protocol_wrapper.closeOpenSessionForDevice(number + '.' + deviceId); - })); - } - - return p.then(function() { - var resetDevices = ((error.code == 410) ? error.response.staleDevices : error.response.missingDevices); - return getKeysForNumber(number, resetDevices) - .then(reloadDevicesAndSend(number, (error.code == 409))) - .catch(function(error) { - registerError(number, "Failed to reload device keys", error); - }); - }); - } else { - registerError(number, "Failed to create or send message", error); - } - }); - }.bind(this); - - function sendToNumber(number) { - return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) { - return Promise.all(devicesForNumber.map(function(device) { - return textsecure.protocol_wrapper.hasOpenSession(device.encodedNumber).then(function(result) { - if (!result) - return getKeysForNumber(number, [parseInt(textsecure.utils.unencodeNumber(device.encodedNumber)[1])]); - }); - })).then(function() { - return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) { - if (devicesForNumber.length == 0) { - return getKeysForNumber(number, [1]) - .then(reloadDevicesAndSend(number, true)) - .catch(function(error) { - registerError(number, "Failed to retreive new device keys for number " + number, error); - }); - } else - return doSendMessage(number, devicesForNumber, true); - }); - }); - }); - } - - numbers.forEach(function(number) { - var sendPrevious = this.pendingMessages[number] || Promise.resolve(); - var sendCurrent = this.pendingMessages[number] = sendPrevious.then(function() { - return sendToNumber(number); - }).catch(function() { - return sendToNumber(number); - }); - sendCurrent.then(function() { - if (this.pendingMessages[number] === sendCurrent) { - delete this.pendingMessages[number]; - } - }.bind(this)); - }.bind(this)); + var outgoing = new OutgoingMessage(timestamp, numbers, message, callback, this); + outgoing.send(); }, sendIndividualProto: function(number, proto, timestamp) { diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js new file mode 100644 index 00000000..39633b99 --- /dev/null +++ b/libtextsecure/outgoing_message.js @@ -0,0 +1,151 @@ +/* + * vim: ts=4:sw=4:expandtab + */ +function OutgoingMessage(timestamp, numbers, message, callback, messageSender) { + this.timestamp = timestamp; + this.numbers = numbers; + this.message = message; + this.sender = messageSender; + this.server = messageSender.server; + this.callback = callback; + + this.numbersCompleted = 0; + this.errors = []; + this.successfulNumbers = []; +} + +OutgoingMessage.prototype = { + constructor: OutgoingMessage, + numberCompleted: function() { + this.numbersCompleted++; + if (this.numbersCompleted >= this.numbers.length) { + this.callback({success: this.successfulNumbers, failure: this.errors}); + } + }, + registerError: function(number, reason, error) { + if (!error || error.name === 'HTTPError') { + error = new textsecure.OutgoingMessageError(number, this.message.toArrayBuffer(), this.timestamp, error); + } + + error.number = number; + error.reason = reason; + this.errors[this.errors.length] = error; + this.numberCompleted(); + }, + reloadDevicesAndSend: function(number, recurse) { + return function() { + return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) { + if (devicesForNumber.length == 0) + return this.registerError(number, "Got empty device list when loading device keys", null); + return this.doSendMessage(number, devicesForNumber, recurse); + }.bind(this)); + }.bind(this); + }, + + getKeysForNumber: function(number, updateDevices) { + var handleResult = function(response) { + return Promise.all(response.devices.map(function(device) { + if (updateDevices === undefined || updateDevices.indexOf(device.deviceId) > -1) + return textsecure.storage.devices.saveKeysToDeviceObject({ + encodedNumber: number + "." + device.deviceId, + identityKey: response.identityKey, + preKey: device.preKey.publicKey, + preKeyId: device.preKey.keyId, + signedKey: device.signedPreKey.publicKey, + signedKeyId: device.signedPreKey.keyId, + signedKeySignature: device.signedPreKey.signature, + registrationId: device.registrationId + }).catch(function(error) { + if (error.message === "Identity key changed") { + error = new textsecure.OutgoingIdentityKeyError(number, this.message.toArrayBuffer(), this.timestamp, error.identityKey); + this.registerError(number, "Identity key changed", error); + } + throw error; + }.bind(this)); + }.bind(this))); + }.bind(this); + + if (updateDevices === undefined) { + return this.server.getKeysForNumber(number).then(handleResult); + } else { + var promises = updateDevices.map(function(device) { + return this.server.getKeysForNumber(number, device).then(handleResult); + }.bind(this)); + + return Promise.all(promises); + } + }, + + doSendMessage: function(number, devicesForNumber, recurse) { + return this.sender.encryptToDevices(this.timestamp, number, devicesForNumber, this.message).then(function(jsonData) { + return this.sender.transmitMessage(number, jsonData).then(function() { + this.successfulNumbers[this.successfulNumbers.length] = number; + this.numberCompleted(); + }.bind(this)); + }.bind(this)).catch(function(error) { + if (error instanceof Error && error.name == "HTTPError" && (error.code == 410 || error.code == 409)) { + if (!recurse) + return this.registerError(number, "Hit retry limit attempting to reload device list", error); + + var p; + if (error.code == 409) { + p = textsecure.storage.devices.removeDeviceIdsForNumber(number, error.response.extraDevices); + } else { + p = Promise.all(error.response.staleDevices.map(function(deviceId) { + return textsecure.protocol_wrapper.closeOpenSessionForDevice(number + '.' + deviceId); + })); + } + + return p.then(function() { + var resetDevices = ((error.code == 410) ? error.response.staleDevices : error.response.missingDevices); + return this.getKeysForNumber(number, resetDevices) + .then(this.reloadDevicesAndSend(number, (error.code == 409))) + .catch(function(error) { + this.registerError(number, "Failed to reload device keys", error); + }.bind(this)); + }.bind(this)); + } else { + this.registerError(number, "Failed to create or send message", error); + } + }.bind(this)); + }, + + sendToNumber: function(number) { + return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) { + return Promise.all(devicesForNumber.map(function(device) { + return textsecure.protocol_wrapper.hasOpenSession(device.encodedNumber).then(function(result) { + if (!result) + return this.getKeysForNumber(number, [parseInt(textsecure.utils.unencodeNumber(device.encodedNumber)[1])]); + }.bind(this)); + }.bind(this))).then(function() { + return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) { + if (devicesForNumber.length == 0) { + return this.getKeysForNumber(number, [1]) + .then(this.reloadDevicesAndSend(number, true)) + .catch(function(error) { + this.registerError(number, "Failed to retreive new device keys for number " + number, error); + }.bind(this)); + } else { + return this.doSendMessage(number, devicesForNumber, true); + } + }.bind(this)); + }.bind(this)); + }.bind(this)); + }, + + send: function() { + this.numbers.forEach(function(number) { + var sendPrevious = this.sender.pendingMessages[number] || Promise.resolve(); + var sendCurrent = this.sender.pendingMessages[number] = sendPrevious.then(function() { + return this.sendToNumber(number); + }.bind(this)).catch(function() { + return this.sendToNumber(number); + }.bind(this)); + sendCurrent.then(function() { + if (this.sender.pendingMessages[number] === sendCurrent) { + delete this.sender.pendingMessages[number]; + } + }.bind(this)); + }.bind(this)); + } +}; diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 9ef7eda2..f379a1fa 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -80,140 +80,8 @@ MessageSender.prototype = { }, sendMessageProto: function(timestamp, numbers, message, callback) { - var numbersCompleted = 0; - var errors = []; - var successfulNumbers = []; - - var numberCompleted = function() { - numbersCompleted++; - if (numbersCompleted >= numbers.length) - callback({success: successfulNumbers, failure: errors}); - }; - - var registerError = function(number, reason, error) { - if (!error || error.name === 'HTTPError') { - error = new textsecure.OutgoingMessageError(number, message.toArrayBuffer(), timestamp, error); - } - - error.number = number; - error.reason = reason; - errors[errors.length] = error; - numberCompleted(); - }; - - var reloadDevicesAndSend = function(number, recurse) { - return function() { - return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) { - if (devicesForNumber.length == 0) - return registerError(number, "Got empty device list when loading device keys", null); - return doSendMessage(number, devicesForNumber, recurse); - }); - } - }; - - var getKeysForNumber = function(number, updateDevices) { - var handleResult = function(response) { - return Promise.all(response.devices.map(function(device) { - if (updateDevices === undefined || updateDevices.indexOf(device.deviceId) > -1) - return textsecure.storage.devices.saveKeysToDeviceObject({ - encodedNumber: number + "." + device.deviceId, - identityKey: response.identityKey, - preKey: device.preKey.publicKey, - preKeyId: device.preKey.keyId, - signedKey: device.signedPreKey.publicKey, - signedKeyId: device.signedPreKey.keyId, - signedKeySignature: device.signedPreKey.signature, - registrationId: device.registrationId - }).catch(function(error) { - if (error.message === "Identity key changed") { - error = new textsecure.OutgoingIdentityKeyError(number, message.toArrayBuffer(), timestamp, error.identityKey); - registerError(number, "Identity key changed", error); - } - throw error; - }); - })); - }; - - if (updateDevices === undefined) { - return this.server.getKeysForNumber(number).then(handleResult); - } else { - var promises = updateDevices.map(function(device) { - return this.server.getKeysForNumber(number, device).then(handleResult); - }.bind(this)); - - return Promise.all(promises); - } - }.bind(this); - - var doSendMessage = function(number, devicesForNumber, recurse) { - return this.encryptToDevices(timestamp, number, devicesForNumber, message).then(function(jsonData) { - return this.transmitMessage(number, jsonData).then(function() { - successfulNumbers[successfulNumbers.length] = number; - numberCompleted(); - }.bind(this)); - }.bind(this)).catch(function(error) { - if (error instanceof Error && error.name == "HTTPError" && (error.code == 410 || error.code == 409)) { - if (!recurse) - return registerError(number, "Hit retry limit attempting to reload device list", error); - - var p; - if (error.code == 409) { - p = textsecure.storage.devices.removeDeviceIdsForNumber(number, error.response.extraDevices); - } else { - p = Promise.all(error.response.staleDevices.map(function(deviceId) { - return textsecure.protocol_wrapper.closeOpenSessionForDevice(number + '.' + deviceId); - })); - } - - return p.then(function() { - var resetDevices = ((error.code == 410) ? error.response.staleDevices : error.response.missingDevices); - return getKeysForNumber(number, resetDevices) - .then(reloadDevicesAndSend(number, (error.code == 409))) - .catch(function(error) { - registerError(number, "Failed to reload device keys", error); - }); - }); - } else { - registerError(number, "Failed to create or send message", error); - } - }); - }.bind(this); - - function sendToNumber(number) { - return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) { - return Promise.all(devicesForNumber.map(function(device) { - return textsecure.protocol_wrapper.hasOpenSession(device.encodedNumber).then(function(result) { - if (!result) - return getKeysForNumber(number, [parseInt(textsecure.utils.unencodeNumber(device.encodedNumber)[1])]); - }); - })).then(function() { - return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) { - if (devicesForNumber.length == 0) { - return getKeysForNumber(number, [1]) - .then(reloadDevicesAndSend(number, true)) - .catch(function(error) { - registerError(number, "Failed to retreive new device keys for number " + number, error); - }); - } else - return doSendMessage(number, devicesForNumber, true); - }); - }); - }); - } - - numbers.forEach(function(number) { - var sendPrevious = this.pendingMessages[number] || Promise.resolve(); - var sendCurrent = this.pendingMessages[number] = sendPrevious.then(function() { - return sendToNumber(number); - }).catch(function() { - return sendToNumber(number); - }); - sendCurrent.then(function() { - if (this.pendingMessages[number] === sendCurrent) { - delete this.pendingMessages[number]; - } - }.bind(this)); - }.bind(this)); + var outgoing = new OutgoingMessage(timestamp, numbers, message, callback, this); + outgoing.send(); }, sendIndividualProto: function(number, proto, timestamp) {