s/\t/ /g in sendmessage.js

This commit is contained in:
Matt Corallo 2014-12-24 18:45:51 -05:00
parent cf99f03acd
commit 406be10f53

View file

@ -15,106 +15,106 @@
*/ */
// sendMessage(numbers = [], message = PushMessageContentProto, callback(success/failure map)) // sendMessage(numbers = [], message = PushMessageContentProto, callback(success/failure map))
window.textsecure.messaging = function() { window.textsecure.messaging = function() {
'use strict'; 'use strict';
var self = {}; var self = {};
function getKeysForNumber(number, updateDevices) { function getKeysForNumber(number, updateDevices) {
var handleResult = function(response) { var handleResult = function(response) {
for (var i in response.devices) { for (var i in response.devices) {
if (updateDevices === undefined || updateDevices.indexOf(response.devices[i].deviceId) > -1) if (updateDevices === undefined || updateDevices.indexOf(response.devices[i].deviceId) > -1)
textsecure.storage.devices.saveKeysToDeviceObject({ textsecure.storage.devices.saveKeysToDeviceObject({
encodedNumber: number + "." + response.devices[i].deviceId, encodedNumber: number + "." + response.devices[i].deviceId,
identityKey: response.identityKey, identityKey: response.identityKey,
preKey: response.devices[i].preKey.publicKey, preKey: response.devices[i].preKey.publicKey,
preKeyId: response.devices[i].preKey.keyId, preKeyId: response.devices[i].preKey.keyId,
signedKey: response.devices[i].signedPreKey.publicKey, signedKey: response.devices[i].signedPreKey.publicKey,
signedKeyId: response.devices[i].signedPreKey.keyId, signedKeyId: response.devices[i].signedPreKey.keyId,
registrationId: response.devices[i].registrationId registrationId: response.devices[i].registrationId
}); });
} }
}; };
var promises = []; var promises = [];
if (updateDevices !== undefined) if (updateDevices !== undefined)
for (var i in updateDevices) for (var i in updateDevices)
promises[promises.length] = textsecure.api.getKeysForNumber(number, updateDevices[i]).then(handleResult); promises[promises.length] = textsecure.api.getKeysForNumber(number, updateDevices[i]).then(handleResult);
else else
return textsecure.api.getKeysForNumber(number).then(handleResult); return textsecure.api.getKeysForNumber(number).then(handleResult);
return Promise.all(promises); return Promise.all(promises);
} }
// success_callback(server success/failure map), error_callback(error_msg) // success_callback(server success/failure map), error_callback(error_msg)
// message == PushMessageContentProto (NOT STRING) // message == PushMessageContentProto (NOT STRING)
function sendMessageToDevices(timestamp, number, deviceObjectList, message, success_callback, error_callback) { function sendMessageToDevices(timestamp, number, deviceObjectList, message, success_callback, error_callback) {
var jsonData = []; var jsonData = [];
var relay = undefined; var relay = undefined;
var promises = []; var promises = [];
var addEncryptionFor = function(i) { var addEncryptionFor = function(i) {
if (deviceObjectList[i].relay !== undefined) { if (deviceObjectList[i].relay !== undefined) {
if (relay === undefined) if (relay === undefined)
relay = deviceObjectList[i].relay; relay = deviceObjectList[i].relay;
else if (relay != deviceObjectList[i].relay) else if (relay != deviceObjectList[i].relay)
return new Promise(function() { throw new Error("Mismatched relays for number " + number); }); return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
} else { } else {
if (relay === undefined) if (relay === undefined)
relay = ""; relay = "";
else if (relay != "") else if (relay != "")
return new Promise(function() { throw new Error("Mismatched relays for number " + number); }); return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
} }
return textsecure.protocol.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) { return textsecure.protocol.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) {
jsonData[i] = { jsonData[i] = {
type: encryptedMsg.type, type: encryptedMsg.type,
destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1], destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1],
destinationRegistrationId: deviceObjectList[i].registrationId, destinationRegistrationId: deviceObjectList[i].registrationId,
body: encryptedMsg.body, body: encryptedMsg.body,
timestamp: timestamp timestamp: timestamp
}; };
if (deviceObjectList[i].relay !== undefined) if (deviceObjectList[i].relay !== undefined)
jsonData[i].relay = deviceObjectList[i].relay; jsonData[i].relay = deviceObjectList[i].relay;
}); });
} }
for (var i = 0; i < deviceObjectList.length; i++) for (var i = 0; i < deviceObjectList.length; i++)
promises[i] = addEncryptionFor(i); promises[i] = addEncryptionFor(i);
return Promise.all(promises).then(function() { return Promise.all(promises).then(function() {
return textsecure.api.sendMessages(number, jsonData); return textsecure.api.sendMessages(number, jsonData);
}); });
} }
var sendGroupProto; var sendGroupProto;
var makeAttachmentPointer; var makeAttachmentPointer;
var refreshGroups = function(number) { var refreshGroups = function(number) {
var groups = textsecure.storage.groups.getGroupListForNumber(number); var groups = textsecure.storage.groups.getGroupListForNumber(number);
var promises = []; var promises = [];
for (var i in groups) { for (var i in groups) {
var group = textsecure.storage.groups.getGroup(groups[i]); var group = textsecure.storage.groups.getGroup(groups[i]);
var proto = new textsecure.protobuf.PushMessageContent(); var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
proto.group.id = toArrayBuffer(group.id); proto.group.id = toArrayBuffer(group.id);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.members = group.numbers; proto.group.members = group.numbers;
proto.group.name = group.name === undefined ? null : group.name; proto.group.name = group.name === undefined ? null : group.name;
if (group.avatar !== undefined) { if (group.avatar !== undefined) {
return makeAttachmentPointer(group.avatar).then(function(attachment) { return makeAttachmentPointer(group.avatar).then(function(attachment) {
proto.group.avatar = attachment; proto.group.avatar = attachment;
promises.push(sendGroupProto([number], proto)); promises.push(sendGroupProto([number], proto));
}); });
} else { } else {
promises.push(sendGroupProto([number], proto)); promises.push(sendGroupProto([number], proto));
} }
} }
return Promise.all(promises); return Promise.all(promises);
} }
var tryMessageAgain = function(number, encodedMessage, message_id) { var tryMessageAgain = function(number, encodedMessage, message_id) {
var message = new Whisper.MessageCollection().add({id: message_id}); var message = new Whisper.MessageCollection().add({id: message_id});
message.fetch().then(function() { message.fetch().then(function() {
textsecure.storage.removeEncrypted("devices" + number); textsecure.storage.removeEncrypted("devices" + number);
@ -131,264 +131,264 @@ window.textsecure.messaging = function() {
}); });
}); });
}); });
}; };
textsecure.replay.registerFunction(tryMessageAgain, textsecure.replay.Type.SEND_MESSAGE); textsecure.replay.registerFunction(tryMessageAgain, textsecure.replay.Type.SEND_MESSAGE);
var sendMessageProto = function(timestamp, numbers, message, callback) { var sendMessageProto = function(timestamp, numbers, message, callback) {
var numbersCompleted = 0; var numbersCompleted = 0;
var errors = []; var errors = [];
var successfulNumbers = []; var successfulNumbers = [];
var numberCompleted = function() { var numberCompleted = function() {
numbersCompleted++; numbersCompleted++;
if (numbersCompleted >= numbers.length) if (numbersCompleted >= numbers.length)
callback({success: successfulNumbers, failure: errors}); callback({success: successfulNumbers, failure: errors});
} }
var registerError = function(number, message, error) { var registerError = function(number, message, error) {
if (error) { if (error) {
if (error.humanError) if (error.humanError)
message = error.humanError; message = error.humanError;
} else } else
error = new Error(message); error = new Error(message);
errors[errors.length] = { number: number, reason: message, error: error }; errors[errors.length] = { number: number, reason: message, error: error };
numberCompleted(); numberCompleted();
} }
var doSendMessage; var doSendMessage;
var reloadDevicesAndSend = function(number, recurse) { var reloadDevicesAndSend = function(number, recurse) {
return function() { return function() {
var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number); var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number);
if (devicesForNumber.length == 0) if (devicesForNumber.length == 0)
return registerError(number, "Got empty device list when loading device keys", null); return registerError(number, "Got empty device list when loading device keys", null);
refreshGroups(number).then(function() { refreshGroups(number).then(function() {
doSendMessage(number, devicesForNumber, recurse); doSendMessage(number, devicesForNumber, recurse);
}); });
} }
} }
doSendMessage = function(number, devicesForNumber, recurse) { doSendMessage = function(number, devicesForNumber, recurse) {
return sendMessageToDevices(timestamp, number, devicesForNumber, message).then(function(result) { return sendMessageToDevices(timestamp, number, devicesForNumber, message).then(function(result) {
successfulNumbers[successfulNumbers.length] = number; successfulNumbers[successfulNumbers.length] = number;
numberCompleted(); numberCompleted();
}).catch(function(error) { }).catch(function(error) {
if (error instanceof Error && error.name == "HTTPError" && (error.message == 410 || error.message == 409)) { if (error instanceof Error && error.name == "HTTPError" && (error.message == 410 || error.message == 409)) {
if (!recurse) if (!recurse)
return registerError(number, "Hit retry limit attempting to reload device list", error); return registerError(number, "Hit retry limit attempting to reload device list", error);
if (error.message == 409) if (error.message == 409)
textsecure.storage.devices.removeDeviceIdsForNumber(number, error.response.extraDevices); textsecure.storage.devices.removeDeviceIdsForNumber(number, error.response.extraDevices);
var resetDevices = ((error.message == 410) ? error.response.staleDevices : error.response.missingDevices); var resetDevices = ((error.message == 410) ? error.response.staleDevices : error.response.missingDevices);
getKeysForNumber(number, resetDevices) getKeysForNumber(number, resetDevices)
.then(reloadDevicesAndSend(number, false)) .then(reloadDevicesAndSend(number, false))
.catch(function(error) { .catch(function(error) {
if (error.message !== "Identity key changed") if (error.message !== "Identity key changed")
registerError(number, "Failed to reload device keys", error); registerError(number, "Failed to reload device keys", error);
else { else {
error = new textsecure.OutgoingIdentityKeyError(number, getString(message.encode())); error = new textsecure.OutgoingIdentityKeyError(number, getString(message.encode()));
registerError(number, "Identity key changed", error); registerError(number, "Identity key changed", error);
} }
}); });
} else } else
registerError(number, "Failed to create or send message", error); registerError(number, "Failed to create or send message", error);
}); });
} }
_.each(numbers, function(number) { _.each(numbers, function(number) {
var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number); var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number);
var promises = []; var promises = [];
for (var j in devicesForNumber) for (var j in devicesForNumber)
if (devicesForNumber[j].registrationId === undefined) if (devicesForNumber[j].registrationId === undefined)
promises[promises.length] = getKeysForNumber(number, [parseInt(textsecure.utils.unencodeNumber(devicesForNumber[j].encodedNumber)[1])]); promises[promises.length] = getKeysForNumber(number, [parseInt(textsecure.utils.unencodeNumber(devicesForNumber[j].encodedNumber)[1])]);
Promise.all(promises).then(function() { Promise.all(promises).then(function() {
devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number); devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number);
if (devicesForNumber.length == 0) { if (devicesForNumber.length == 0) {
getKeysForNumber(number) getKeysForNumber(number)
.then(reloadDevicesAndSend(number, true)) .then(reloadDevicesAndSend(number, true))
.catch(function(error) { .catch(function(error) {
registerError(number, "Failed to retreive new device keys for number " + number, error); registerError(number, "Failed to retreive new device keys for number " + number, error);
}); });
} else } else
doSendMessage(number, devicesForNumber, true); doSendMessage(number, devicesForNumber, true);
}); });
}); });
} }
makeAttachmentPointer = function(attachment) { makeAttachmentPointer = function(attachment) {
var proto = new textsecure.protobuf.PushMessageContent.AttachmentPointer(); var proto = new textsecure.protobuf.PushMessageContent.AttachmentPointer();
proto.key = textsecure.crypto.getRandomBytes(64); proto.key = textsecure.crypto.getRandomBytes(64);
var iv = textsecure.crypto.getRandomBytes(16); var iv = textsecure.crypto.getRandomBytes(16);
return textsecure.protocol.encryptAttachment(attachment.data, proto.key, iv).then(function(encryptedBin) { return textsecure.protocol.encryptAttachment(attachment.data, proto.key, iv).then(function(encryptedBin) {
return textsecure.api.putAttachment(encryptedBin).then(function(id) { return textsecure.api.putAttachment(encryptedBin).then(function(id) {
proto.id = id; proto.id = id;
proto.contentType = attachment.contentType; proto.contentType = attachment.contentType;
return proto; return proto;
}); });
}); });
} }
var sendIndividualProto = function(number, proto, timestamp) { var sendIndividualProto = function(number, proto, timestamp) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
sendMessageProto(timestamp, [number], proto, function(res) { sendMessageProto(timestamp, [number], proto, function(res) {
if (res.failure.length > 0) if (res.failure.length > 0)
reject(res.failure); reject(res.failure);
else else
resolve(); resolve();
}); });
}); });
} }
sendGroupProto = function(numbers, proto, timestamp) { sendGroupProto = function(numbers, proto, timestamp) {
timestamp = timestamp || Date.now(); timestamp = timestamp || Date.now();
var me = textsecure.utils.unencodeNumber(textsecure.storage.getUnencrypted("number_id"))[0]; var me = textsecure.utils.unencodeNumber(textsecure.storage.getUnencrypted("number_id"))[0];
numbers = numbers.filter(function(number) { return number != me; }); numbers = numbers.filter(function(number) { return number != me; });
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
sendMessageProto(timestamp, numbers, proto, function(res) { sendMessageProto(timestamp, numbers, proto, function(res) {
if (res.failure.length > 0) if (res.failure.length > 0)
reject(res.failure); reject(res.failure);
else else
resolve(); resolve();
}); });
}); });
} }
self.sendMessageToNumber = function(number, messageText, attachments, timestamp) { self.sendMessageToNumber = function(number, messageText, attachments, timestamp) {
var proto = new textsecure.protobuf.PushMessageContent(); var proto = new textsecure.protobuf.PushMessageContent();
proto.body = messageText; proto.body = messageText;
var promises = []; var promises = [];
for (var i in attachments) for (var i in attachments)
promises.push(makeAttachmentPointer(attachments[i])); promises.push(makeAttachmentPointer(attachments[i]));
return Promise.all(promises).then(function(attachmentsArray) { return Promise.all(promises).then(function(attachmentsArray) {
proto.attachments = attachmentsArray; proto.attachments = attachmentsArray;
return sendIndividualProto(number, proto, timestamp); return sendIndividualProto(number, proto, timestamp);
}); });
} }
self.closeSession = function(number) { self.closeSession = function(number) {
var proto = new textsecure.protobuf.PushMessageContent(); var proto = new textsecure.protobuf.PushMessageContent();
proto.body = "TERMINATE"; proto.body = "TERMINATE";
proto.flags = textsecure.protobuf.PushMessageContent.Flags.END_SESSION; proto.flags = textsecure.protobuf.PushMessageContent.Flags.END_SESSION;
return sendIndividualProto(number, proto).then(function(res) { return sendIndividualProto(number, proto).then(function(res) {
var devices = textsecure.storage.devices.getDeviceObjectsForNumber(number); var devices = textsecure.storage.devices.getDeviceObjectsForNumber(number);
for (var i in devices) for (var i in devices)
textsecure.protocol.closeOpenSessionForDevice(devices[i].encodedNumber); textsecure.protocol.closeOpenSessionForDevice(devices[i].encodedNumber);
return res; return res;
}); });
} }
self.sendMessageToGroup = function(groupId, messageText, attachments, timestamp) { self.sendMessageToGroup = function(groupId, messageText, attachments, timestamp) {
var proto = new textsecure.protobuf.PushMessageContent(); var proto = new textsecure.protobuf.PushMessageContent();
proto.body = messageText; proto.body = messageText;
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
proto.group.id = toArrayBuffer(groupId); proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER; proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER;
var numbers = textsecure.storage.groups.getNumbers(groupId); var numbers = textsecure.storage.groups.getNumbers(groupId);
if (numbers === undefined) if (numbers === undefined)
return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); }); return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); });
var promises = []; var promises = [];
for (var i in attachments) for (var i in attachments)
promises.push(makeAttachmentPointer(attachments[i])); promises.push(makeAttachmentPointer(attachments[i]));
return Promise.all(promises).then(function(attachmentsArray) { return Promise.all(promises).then(function(attachmentsArray) {
proto.attachments = attachmentsArray; proto.attachments = attachmentsArray;
return sendGroupProto(numbers, proto, timestamp); return sendGroupProto(numbers, proto, timestamp);
}); });
} }
self.createGroup = function(numbers, name, avatar) { self.createGroup = function(numbers, name, avatar) {
var proto = new textsecure.protobuf.PushMessageContent(); var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var group = textsecure.storage.groups.createNewGroup(numbers); var group = textsecure.storage.groups.createNewGroup(numbers);
proto.group.id = toArrayBuffer(group.id); proto.group.id = toArrayBuffer(group.id);
var numbers = group.numbers; var numbers = group.numbers;
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.members = numbers; proto.group.members = numbers;
proto.group.name = name; proto.group.name = name;
if (avatar !== undefined) { if (avatar !== undefined) {
return makeAttachmentPointer(avatar).then(function(attachment) { return makeAttachmentPointer(avatar).then(function(attachment) {
proto.group.avatar = attachment; proto.group.avatar = attachment;
return sendGroupProto(numbers, proto).then(function() { return sendGroupProto(numbers, proto).then(function() {
return proto.group.id; return proto.group.id;
}); });
}); });
} else { } else {
return sendGroupProto(numbers, proto).then(function() { return sendGroupProto(numbers, proto).then(function() {
return proto.group.id; return proto.group.id;
}); });
} }
} }
self.addNumberToGroup = function(groupId, number) { self.addNumberToGroup = function(groupId, number) {
var proto = new textsecure.protobuf.PushMessageContent(); var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
proto.group.id = toArrayBuffer(groupId); proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
var numbers = textsecure.storage.groups.addNumbers(groupId, [number]); var numbers = textsecure.storage.groups.addNumbers(groupId, [number]);
if (numbers === undefined) if (numbers === undefined)
return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); }); return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); });
proto.group.members = numbers; proto.group.members = numbers;
return sendGroupProto(numbers, proto); return sendGroupProto(numbers, proto);
} }
self.setGroupName = function(groupId, name) { self.setGroupName = function(groupId, name) {
var proto = new textsecure.protobuf.PushMessageContent(); var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
proto.group.id = toArrayBuffer(groupId); proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.name = name; proto.group.name = name;
var numbers = textsecure.storage.groups.getNumbers(groupId); var numbers = textsecure.storage.groups.getNumbers(groupId);
if (numbers === undefined) if (numbers === undefined)
return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); }); return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); });
proto.group.members = numbers; proto.group.members = numbers;
return sendGroupProto(numbers, proto); return sendGroupProto(numbers, proto);
} }
self.setGroupAvatar = function(groupId, avatar) { self.setGroupAvatar = function(groupId, avatar) {
var proto = new textsecure.protobuf.PushMessageContent(); var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
proto.group.id = toArrayBuffer(groupId); proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
var numbers = textsecure.storage.groups.getNumbers(groupId); var numbers = textsecure.storage.groups.getNumbers(groupId);
if (numbers === undefined) if (numbers === undefined)
return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); }); return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); });
proto.group.members = numbers; proto.group.members = numbers;
return makeAttachmentPointer(avatar).then(function(attachment) { return makeAttachmentPointer(avatar).then(function(attachment) {
proto.group.avatar = attachment; proto.group.avatar = attachment;
return sendGroupProto(numbers, proto); return sendGroupProto(numbers, proto);
}); });
} }
self.leaveGroup = function(groupId) { self.leaveGroup = function(groupId) {
var proto = new textsecure.protobuf.PushMessageContent(); var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
proto.group.id = toArrayBuffer(groupId); proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT; proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT;
var numbers = textsecure.storage.groups.getNumbers(groupId); var numbers = textsecure.storage.groups.getNumbers(groupId);
if (numbers === undefined) if (numbers === undefined)
return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); }); return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); });
textsecure.storage.groups.deleteGroup(groupId); textsecure.storage.groups.deleteGroup(groupId);
return sendGroupProto(numbers, proto); return sendGroupProto(numbers, proto);
} }
return self; return self;
}(); }();