signal-desktop/libtextsecure/sendmessage.js
lilia 6123c419d0 Remove refreshGroup
1. This is nonstandard behavior, not supported by any other clients. It
   may help sometimes but will also cause bugs (see 2)

2. iOS doesn't handle group updates with missing fields. all fields must
   be populated, and libtextsecure doesn't have any knowledge of the group
   name or avatar, so these updates will clobber group state on iOS.

// FREEBIE
2015-09-23 16:45:09 -07:00

437 lines
19 KiB
JavaScript

/*
* vim: ts=4:sw=4:expandtab
*/
// sendMessage(numbers = [], message = PushMessageContentProto, callback(success/failure map))
window.textsecure.messaging = function() {
'use strict';
var self = {};
// message == DataMessage or ContentMessage proto
function sendMessageToDevices(timestamp, number, deviceObjectList, message) {
var relay = deviceObjectList[0].relay;
for (var i=1; i < deviceObjectList.length; ++i) {
if (deviceObjectList[i].relay !== relay) {
throw new Error("Mismatched relays for number " + number);
}
}
return Promise.all(deviceObjectList.map(function(device) {
return textsecure.protocol_wrapper.encryptMessageFor(device, message).then(function(encryptedMsg) {
return textsecure.protocol_wrapper.getRegistrationId(device.encodedNumber).then(function(registrationId) {
return textsecure.storage.devices.removeTempKeysFromDevice(device.encodedNumber).then(function() {
var json = {
type: encryptedMsg.type,
destinationDeviceId: textsecure.utils.unencodeNumber(device.encodedNumber)[1],
destinationRegistrationId: registrationId,
content: encryptedMsg.body,
timestamp: timestamp
};
if (device.relay !== undefined) {
json.relay = device.relay;
}
return json;
});
});
});
})).then(function(jsonData) {
var legacy = (message instanceof textsecure.protobuf.DataMessage);
return TextSecureServer.sendMessages(number, jsonData, legacy);
});
}
function makeAttachmentPointer(attachment) {
var proto = new textsecure.protobuf.AttachmentPointer();
proto.key = textsecure.crypto.getRandomBytes(64);
var iv = textsecure.crypto.getRandomBytes(16);
return textsecure.crypto.encryptAttachment(attachment.data, proto.key, iv).then(function(encryptedBin) {
return TextSecureServer.putAttachment(encryptedBin).then(function(id) {
proto.id = id;
proto.contentType = attachment.contentType;
return proto;
});
});
}
var tryMessageAgain = function(number, encodedMessage, timestamp) {
var proto = textsecure.protobuf.DataMessage.decode(encodedMessage);
return new Promise(function(resolve, reject) {
sendMessageProto(timestamp, [number], proto, function(res) {
if (res.failure.length > 0)
reject(res.failure);
else
resolve();
});
});
};
textsecure.replay.registerFunction(tryMessageAgain, textsecure.replay.Type.SEND_MESSAGE);
var 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, message, error) {
if (!error) {
error = new Error(message);
}
errors[errors.length] = { number: number, reason: message, error: 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);
doSendMessage(number, devicesForNumber, recurse);
});
}
};
function getKeysForNumber(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 TextSecureServer.getKeysForNumber(number).then(handleResult);
} else {
var promises = [];
for (var i in updateDevices)
promises[promises.length] = TextSecureServer.getKeysForNumber(number, updateDevices[i]).then(handleResult);
return Promise.all(promises);
}
}
var doSendMessage = function(number, devicesForNumber, recurse) {
return sendMessageToDevices(timestamp, number, devicesForNumber, message).then(function(result) {
successfulNumbers[successfulNumbers.length] = number;
numberCompleted();
}).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);
}));
}
p.then(function() {
var resetDevices = ((error.code == 410) ? error.response.staleDevices : error.response.missingDevices);
getKeysForNumber(number, resetDevices)
.then(reloadDevicesAndSend(number, false))
.catch(function(error) {
registerError(number, "Failed to reload device keys", error);
});
});
} else {
registerError(number, "Failed to create or send message", error);
}
});
};
numbers.forEach(function(number) {
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) {
getKeysForNumber(number, [1])
.then(reloadDevicesAndSend(number, true))
.catch(function(error) {
registerError(number, "Failed to retreive new device keys for number " + number, error);
});
} else
doSendMessage(number, devicesForNumber, true);
});
});
});
});
}
var sendIndividualProto = function(number, proto, timestamp) {
return new Promise(function(resolve, reject) {
sendMessageProto(timestamp, [number], proto, function(res) {
if (res.failure.length > 0)
reject(res.failure);
else
resolve();
});
});
}
var sendSyncMessage = function(message, timestamp, destination) {
var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId();
if (myDevice != 1) {
var sentMessage = new textsecure.protobuf.SyncMessage.Sent();
sentMessage.timestamp = timestamp;
sentMessage.message = message;
if (destination) {
sentMessage.destination = destination;
}
var syncMessage = new textsecure.protobuf.SyncMessage();
syncMessage.sent = sentMessage;
var contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
return sendIndividualProto(myNumber, contentMessage, Date.now());
}
}
self.sendRequestGroupSyncMessage = function() {
var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId();
if (myDevice != 1) {
var request = new textsecure.protobuf.SyncMessage.Request();
request.type = textsecure.protobuf.SyncMessage.Request.Type.GROUPS;
var syncMessage = new textsecure.protobuf.SyncMessage();
syncMessage.request = request;
var contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
return sendIndividualProto(myNumber, contentMessage, Date.now());
}
};
self.sendRequestContactSyncMessage = function() {
var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId();
if (myDevice != 1) {
var request = new textsecure.protobuf.SyncMessage.Request();
request.type = textsecure.protobuf.SyncMessage.Request.Type.CONTACTS;
var syncMessage = new textsecure.protobuf.SyncMessage();
syncMessage.request = request;
var contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
return sendIndividualProto(myNumber, contentMessage, Date.now());
}
};
var sendGroupProto = function(numbers, proto, timestamp) {
timestamp = timestamp || Date.now();
var me = textsecure.storage.user.getNumber();
numbers = numbers.filter(function(number) { return number != me; });
return new Promise(function(resolve, reject) {
sendMessageProto(timestamp, numbers, proto, function(res) {
if (res.failure.length > 0)
reject(res.failure);
else
resolve();
});
}).then(function() {
return sendSyncMessage(proto, timestamp);
});
}
self.sendMessageToNumber = function(number, messageText, attachments, timestamp) {
var proto = new textsecure.protobuf.DataMessage();
proto.body = messageText;
var promises = [];
for (var i in attachments)
promises.push(makeAttachmentPointer(attachments[i]));
return Promise.all(promises).then(function(attachmentsArray) {
proto.attachments = attachmentsArray;
return sendIndividualProto(number, proto, timestamp).then(function() {
return sendSyncMessage(proto, timestamp, number);
});
});
}
self.closeSession = function(number) {
var proto = new textsecure.protobuf.DataMessage();
proto.body = "TERMINATE";
proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION;
return sendIndividualProto(number, proto, Date.now()).then(function(res) {
return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devices) {
return Promise.all(devices.map(function(device) {
return textsecure.protocol_wrapper.closeOpenSessionForDevice(device.encodedNumber);
})).then(function() {
return res;
});
});
});
}
self.sendMessageToGroup = function(groupId, messageText, attachments, timestamp) {
var proto = new textsecure.protobuf.DataMessage();
proto.body = messageText;
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.DELIVER;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
return Promise.reject(new Error("Unknown Group"));
var promises = [];
for (var i in attachments)
promises.push(makeAttachmentPointer(attachments[i]));
return Promise.all(promises).then(function(attachmentsArray) {
proto.attachments = attachmentsArray;
return sendGroupProto(numbers, proto, timestamp);
});
});
}
self.createGroup = function(numbers, name, avatar) {
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
return textsecure.storage.groups.createNewGroup(numbers).then(function(group) {
proto.group.id = toArrayBuffer(group.id);
var numbers = group.numbers;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = numbers;
proto.group.name = name;
if (avatar !== undefined) {
return makeAttachmentPointer(avatar).then(function(attachment) {
proto.group.avatar = attachment;
return sendGroupProto(numbers, proto).then(function() {
return proto.group.id;
});
});
} else {
return sendGroupProto(numbers, proto).then(function() {
return proto.group.id;
});
}
});
}
self.updateGroup = function(groupId, name, avatar, numbers) {
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
return textsecure.storage.groups.addNumbers(groupId, numbers).then(function(numbers) {
if (numbers === undefined) {
return Promise.reject(new Error("Unknown Group"));
}
proto.group.members = numbers;
if (avatar !== undefined && avatar !== null) {
return makeAttachmentPointer(avatar).then(function(attachment) {
proto.group.avatar = attachment;
return sendGroupProto(numbers, proto).then(function() {
return proto.group.id;
});
});
} else {
return sendGroupProto(numbers, proto).then(function() {
return proto.group.id;
});
}
});
}
self.addNumberToGroup = function(groupId, number) {
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
return textsecure.storage.groups.addNumbers(groupId, [number]).then(function(numbers) {
if (numbers === undefined)
return Promise.reject(new Error("Unknown Group"));
proto.group.members = numbers;
return sendGroupProto(numbers, proto);
});
}
self.setGroupName = function(groupId, name) {
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
return Promise.reject(new Error("Unknown Group"));
proto.group.members = numbers;
return sendGroupProto(numbers, proto);
});
}
self.setGroupAvatar = function(groupId, avatar) {
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
return Promise.reject(new Error("Unknown Group"));
proto.group.members = numbers;
return makeAttachmentPointer(avatar).then(function(attachment) {
proto.group.avatar = attachment;
return sendGroupProto(numbers, proto);
});
});
}
self.leaveGroup = function(groupId) {
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
return Promise.reject(new Error("Unknown Group"));
return textsecure.storage.groups.deleteGroup(groupId).then(function() {
return sendGroupProto(numbers, proto);
});
});
}
return self;
}();