/* * vim: ts=4:sw=4:expandtab */ function OutgoingMessage(server, timestamp, numbers, message, callback) { this.server = server; this.timestamp = timestamp; this.numbers = numbers; this.message = message; // DataMessage or ContentMessage proto this.callback = callback; this.legacy = (message instanceof textsecure.protobuf.DataMessage); this.numbersCompleted = 0; this.errors = []; this.successfulNumbers = []; } OutgoingMessage.prototype = { constructor: OutgoingMessage, numberCompleted: function() { this.numbersCompleted++; if (this.numbersCompleted >= this.numbers.length) { this.callback({successfulNumbers: this.successfulNumbers, errors: 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); } var relay = devicesForNumber[0].relay; for (var i=1; i < devicesForNumber.length; ++i) { if (devicesForNumber[i].relay !== relay) { throw new Error("Mismatched relays for number " + number); } } 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 promise = Promise.resolve(); updateDevices.forEach(function(device) { promise = promise.then(function() { return this.server.getKeysForNumber(number, device).then(handleResult); }.bind(this)); }.bind(this)); return promise; } }, transmitMessage: function(number, jsonData, timestamp) { return this.server.sendMessages(number, jsonData, timestamp).catch(function(e) { if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) { // 409 and 410 should bubble and be handled by doSendMessage // all other network errors can be retried later. throw new textsecure.SendMessageNetworkError(number, jsonData, e, timestamp); } throw e; }); }, doSendMessage: function(number, devicesForNumber, recurse) { return this.encryptToDevices(devicesForNumber).then(function(jsonData) { return this.transmitMessage(number, jsonData, this.timestamp).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)); }, encryptToDevices: function(deviceObjectList) { var plaintext = this.message.toArrayBuffer(); return Promise.all(deviceObjectList.map(function(device) { return textsecure.protocol_wrapper.encryptMessageFor(device, plaintext).then(function(encryptedMsg) { var json = this.toJSON(device, encryptedMsg); return textsecure.storage.devices.removeTempKeysFromDevice(device.encodedNumber).then(function() { return json; }); }.bind(this)); }.bind(this))); }, toJSON: function(device, encryptedMsg) { var json = { type: encryptedMsg.type, destinationDeviceId: textsecure.utils.unencodeNumber(device.encodedNumber)[1], destinationRegistrationId: device.registrationId }; if (device.relay !== undefined) { json.relay = device.relay; } var content = btoa(encryptedMsg.body); if (this.legacy) { json.body = content; } else { json.content = content; } return json; }, sendToNumber: function(number) { return textsecure.storage.devices.getStaleDeviceIdsForNumber(number).then(function(updateDevices) { return this.getKeysForNumber(number, updateDevices) .then(this.reloadDevicesAndSend(number, true)) .catch(function(error) { this.registerError(number, "Failed to retreive new device keys for number " + number, error); }.bind(this)); }.bind(this)); } };