From 5925c2fe8426ac251ecd8b9ebf9d077bcaf689ed Mon Sep 17 00:00:00 2001 From: lilia Date: Mon, 22 Jun 2015 14:45:42 -0700 Subject: [PATCH] Support for group sync Protocol and handling is all analogous to contact sync: Multiple GroupDetails structs are packed into a single attachment blob and parsed on our end. We don't display the synced groups in the conversation list until a new message is sent to one of them. // FREEBIE --- js/background.js | 19 ++-- js/libtextsecure.js | 105 ++++++++++++++++----- js/views/file_input_view.js | 2 +- libtextsecure/account_manager.js | 3 +- libtextsecure/contacts_parser.js | 42 ++++++--- libtextsecure/message_receiver.js | 44 ++++++--- libtextsecure/sendmessage.js | 16 +++- libtextsecure/test/contacts_parser_test.js | 60 +++++++++++- protos/IncomingPushMessageSignal.proto | 24 +++-- 9 files changed, 246 insertions(+), 69 deletions(-) diff --git a/js/background.js b/js/background.js index 6abb2bf70cc..541852e2068 100644 --- a/js/background.js +++ b/js/background.js @@ -49,28 +49,29 @@ window.addEventListener('group', onGroupReceived); window.addEventListener('sent', onSentMessage); window.addEventListener('error', onError); + // initialize the socket and start listening for messages messageReceiver = new textsecure.MessageReceiver(window); } function onContactReceived(ev) { - var contactInfo = ev.contactInfo; + var contactDetails = ev.contactDetails; new Whisper.Conversation({ - name: contactInfo.name, - id: contactInfo.number, - avatar: contactInfo.avatar, + name: contactDetails.name, + id: contactDetails.number, + avatar: contactDetails.avatar, type: 'private', active_at: null }).save(); } function onGroupReceived(ev) { - var group = ev.group; + var groupDetails = ev.groupDetails; new Whisper.Conversation({ - members: group.members, - name: group.name, - id: group.id, - avatar: group.avatar, + id: groupDetails.id, + name: groupDetails.name, + members: groupDetails.members, + avatar: groupDetails.avatar, type: 'group', active_at: null }).save(); diff --git a/js/libtextsecure.js b/js/libtextsecure.js index c9927e34787..5c175c4f0ec 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -39339,7 +39339,8 @@ TextSecureServer = function () { then(function() { return generateKeys(100); }). then(TextSecureServer.registerKeys). then(textsecure.registration.done). - then(textsecure.messaging.sendRequestContactSyncMessage); + then(textsecure.messaging.sendRequestContactSyncMessage). + then(textsecure.messaging.sendRequestGroupSyncMessage); }); }, registerSecondDevice: function(setProvisioningUrl, confirmNumber, progressCallback) { @@ -39638,10 +39639,10 @@ function generateKeys(count, progressCallback) { ); } else if (syncMessage.contacts) { this.handleContacts(syncMessage.contacts); - } else if (syncMessage.group) { - this.handleGroup(syncMessage.group); + } else if (syncMessage.groups) { + this.handleGroups(syncMessage.groups); } else { - throw new Error('Got SyncMessage with no sent, contacts, or group'); + throw new Error('Got SyncMessage with no sent, contacts, or groups'); } }, handleContacts: function(contacts) { @@ -39649,19 +39650,41 @@ function generateKeys(count, progressCallback) { var attachmentPointer = contacts.blob; return handleAttachment(attachmentPointer).then(function() { var contactBuffer = new ContactBuffer(attachmentPointer.data); - var contactInfo = contactBuffer.readContact(); - while (contactInfo !== undefined) { + var contactDetails = contactBuffer.next(); + while (contactDetails !== undefined) { var ev = new Event('contact'); - ev.contactInfo = contactInfo; + ev.contactDetails = contactDetails; eventTarget.dispatchEvent(ev); - contactInfo = contactBuffer.readContact(); + contactDetails = contactBuffer.next(); } }); }, - handleGroup: function(envelope) { - var ev = new Event('group'); - ev.group = envelope.group; - this.target.dispatchEvent(ev); + handleGroups: function(groups) { + var eventTarget = this.target; + var attachmentPointer = groups.blob; + return handleAttachment(attachmentPointer).then(function() { + var groupBuffer = new GroupBuffer(attachmentPointer.data); + var groupDetails = groupBuffer.next(); + while (groupDetails !== undefined) { + (function(groupDetails) { + groupDetails.id = getString(groupDetails.id); + textsecure.storage.groups.getGroup(groupDetails.id). + then(function(existingGroup) { + if (existingGroup === undefined) { + return textsecure.storage.groups.createNewGroup( + groupDetails.members, groupDetails.id + ); + } else { + } + }).then(function() { + var ev = new Event('group'); + ev.groupDetails = groupDetails; + eventTarget.dispatchEvent(ev); + }); + })(groupDetails); + groupDetails = groupBuffer.next(); + } + }); } }; @@ -39970,6 +39993,20 @@ window.textsecure.messaging = function() { } } + 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(); @@ -40094,7 +40131,7 @@ window.textsecure.messaging = function() { } proto.group.members = numbers; - if (avatar !== undefined) { + if (avatar !== undefined && avatar !== null) { return makeAttachmentPointer(avatar).then(function(attachment) { proto.group.avatar = attachment; return sendGroupProto(numbers, proto).then(function() { @@ -40179,33 +40216,53 @@ window.textsecure.messaging = function() { /* * vim: ts=4:sw=4:expandtab */ -function ContactBuffer(arrayBuffer) { + +function ProtoParser(arrayBuffer, protobuf) { + this.protobuf = protobuf; this.buffer = new dcodeIO.ByteBuffer(); this.buffer.append(arrayBuffer); this.buffer.offset = 0; this.buffer.limit = arrayBuffer.byteLength; } -ContactBuffer.prototype = { - constructor: ContactBuffer, - readContact: function() { +ProtoParser.prototype = { + constructor: ProtoParser, + next: function() { try { if (this.buffer.limit === this.buffer.offset) { return undefined; // eof } - var len = this.buffer.readVarint64().toNumber(); - var contactInfoBuffer = this.buffer.slice(this.buffer.offset, this.buffer.offset+len); - var contactInfo = textsecure.protobuf.ContactDetails.decode(contactInfoBuffer); + var len = this.buffer.readVarint32(); + var nextBuffer = this.buffer.slice( + this.buffer.offset, this.buffer.offset+len + ).toArrayBuffer(); + // TODO: de-dupe ByteBuffer.js includes in libaxo/libts + // then remove this toArrayBuffer call. + + var proto = this.protobuf.decode(nextBuffer); this.buffer.skip(len); - if (contactInfo.avatar) { - var attachmentLen = contactInfo.avatar.length.toNumber(); - contactInfo.avatar.data = this.buffer.slice(this.buffer.offset, this.buffer.offset + attachmentLen).toArrayBuffer(true); + + if (proto.avatar) { + var attachmentLen = proto.avatar.length; + proto.avatar.data = this.buffer.slice( + this.buffer.offset, this.buffer.offset + attachmentLen + ).toArrayBuffer(); this.buffer.skip(attachmentLen); } - return contactInfo; + return proto; } catch(e) { console.log(e); } } }; +var GroupBuffer = function(arrayBuffer) { + ProtoParser.call(this, arrayBuffer, textsecure.protobuf.GroupDetails); +}; +GroupBuffer.prototype = Object.create(ProtoParser.prototype); +GroupBuffer.prototype.constructor = GroupBuffer; +var ContactBuffer = function(arrayBuffer) { + ProtoParser.call(this, arrayBuffer, textsecure.protobuf.ContactDetails); +}; +ContactBuffer.prototype = Object.create(ProtoParser.prototype); +ContactBuffer.prototype.constructor = ContactBuffer; })(); diff --git a/js/views/file_input_view.js b/js/views/file_input_view.js index 901db54d769..9bd3625da49 100644 --- a/js/views/file_input_view.js +++ b/js/views/file_input_view.js @@ -166,7 +166,7 @@ // Scale and crop an image to 256px square var size = 256; var file = this.file || this.$input.prop('files')[0]; - if (file.type.split('/')[0] !== 'image' || file.type === 'image/gif') { + if (file === undefined || file.type.split('/')[0] !== 'image' || file.type === 'image/gif') { // nothing to do return Promise.resolve(); } diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index cfe02c25e2f..35c97a4eeac 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -36,7 +36,8 @@ then(function() { return generateKeys(100); }). then(TextSecureServer.registerKeys). then(textsecure.registration.done). - then(textsecure.messaging.sendRequestContactSyncMessage); + then(textsecure.messaging.sendRequestContactSyncMessage). + then(textsecure.messaging.sendRequestGroupSyncMessage); }); }, registerSecondDevice: function(setProvisioningUrl, confirmNumber, progressCallback) { diff --git a/libtextsecure/contacts_parser.js b/libtextsecure/contacts_parser.js index 983f1408de1..5ca051336ad 100644 --- a/libtextsecure/contacts_parser.js +++ b/libtextsecure/contacts_parser.js @@ -1,32 +1,52 @@ /* * vim: ts=4:sw=4:expandtab */ -function ContactBuffer(arrayBuffer) { + +function ProtoParser(arrayBuffer, protobuf) { + this.protobuf = protobuf; this.buffer = new dcodeIO.ByteBuffer(); this.buffer.append(arrayBuffer); this.buffer.offset = 0; this.buffer.limit = arrayBuffer.byteLength; } -ContactBuffer.prototype = { - constructor: ContactBuffer, - readContact: function() { +ProtoParser.prototype = { + constructor: ProtoParser, + next: function() { try { if (this.buffer.limit === this.buffer.offset) { return undefined; // eof } - var len = this.buffer.readVarint64().toNumber(); - var contactInfoBuffer = this.buffer.slice(this.buffer.offset, this.buffer.offset+len); - var contactInfo = textsecure.protobuf.ContactDetails.decode(contactInfoBuffer); + var len = this.buffer.readVarint32(); + var nextBuffer = this.buffer.slice( + this.buffer.offset, this.buffer.offset+len + ).toArrayBuffer(); + // TODO: de-dupe ByteBuffer.js includes in libaxo/libts + // then remove this toArrayBuffer call. + + var proto = this.protobuf.decode(nextBuffer); this.buffer.skip(len); - if (contactInfo.avatar) { - var attachmentLen = contactInfo.avatar.length.toNumber(); - contactInfo.avatar.data = this.buffer.slice(this.buffer.offset, this.buffer.offset + attachmentLen).toArrayBuffer(true); + + if (proto.avatar) { + var attachmentLen = proto.avatar.length; + proto.avatar.data = this.buffer.slice( + this.buffer.offset, this.buffer.offset + attachmentLen + ).toArrayBuffer(); this.buffer.skip(attachmentLen); } - return contactInfo; + return proto; } catch(e) { console.log(e); } } }; +var GroupBuffer = function(arrayBuffer) { + ProtoParser.call(this, arrayBuffer, textsecure.protobuf.GroupDetails); +}; +GroupBuffer.prototype = Object.create(ProtoParser.prototype); +GroupBuffer.prototype.constructor = GroupBuffer; +var ContactBuffer = function(arrayBuffer) { + ProtoParser.call(this, arrayBuffer, textsecure.protobuf.ContactDetails); +}; +ContactBuffer.prototype = Object.create(ProtoParser.prototype); +ContactBuffer.prototype.constructor = ContactBuffer; diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 972b584fc55..2b3c91445ec 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -166,10 +166,10 @@ ); } else if (syncMessage.contacts) { this.handleContacts(syncMessage.contacts); - } else if (syncMessage.group) { - this.handleGroup(syncMessage.group); + } else if (syncMessage.groups) { + this.handleGroups(syncMessage.groups); } else { - throw new Error('Got SyncMessage with no sent, contacts, or group'); + throw new Error('Got SyncMessage with no sent, contacts, or groups'); } }, handleContacts: function(contacts) { @@ -177,19 +177,41 @@ var attachmentPointer = contacts.blob; return handleAttachment(attachmentPointer).then(function() { var contactBuffer = new ContactBuffer(attachmentPointer.data); - var contactInfo = contactBuffer.readContact(); - while (contactInfo !== undefined) { + var contactDetails = contactBuffer.next(); + while (contactDetails !== undefined) { var ev = new Event('contact'); - ev.contactInfo = contactInfo; + ev.contactDetails = contactDetails; eventTarget.dispatchEvent(ev); - contactInfo = contactBuffer.readContact(); + contactDetails = contactBuffer.next(); } }); }, - handleGroup: function(envelope) { - var ev = new Event('group'); - ev.group = envelope.group; - this.target.dispatchEvent(ev); + handleGroups: function(groups) { + var eventTarget = this.target; + var attachmentPointer = groups.blob; + return handleAttachment(attachmentPointer).then(function() { + var groupBuffer = new GroupBuffer(attachmentPointer.data); + var groupDetails = groupBuffer.next(); + while (groupDetails !== undefined) { + (function(groupDetails) { + groupDetails.id = getString(groupDetails.id); + textsecure.storage.groups.getGroup(groupDetails.id). + then(function(existingGroup) { + if (existingGroup === undefined) { + return textsecure.storage.groups.createNewGroup( + groupDetails.members, groupDetails.id + ); + } else { + } + }).then(function() { + var ev = new Event('group'); + ev.groupDetails = groupDetails; + eventTarget.dispatchEvent(ev); + }); + })(groupDetails); + groupDetails = groupBuffer.next(); + } + }); } }; diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 48940d0a443..23328239b99 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -290,6 +290,20 @@ window.textsecure.messaging = function() { } } + 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(); @@ -414,7 +428,7 @@ window.textsecure.messaging = function() { } proto.group.members = numbers; - if (avatar !== undefined) { + if (avatar !== undefined && avatar !== null) { return makeAttachmentPointer(avatar).then(function(attachment) { proto.group.avatar = attachment; return sendGroupProto(numbers, proto).then(function() { diff --git a/libtextsecure/test/contacts_parser_test.js b/libtextsecure/test/contacts_parser_test.js index 43e29dd1fc0..a6a31e072d3 100644 --- a/libtextsecure/test/contacts_parser_test.js +++ b/libtextsecure/test/contacts_parser_test.js @@ -16,7 +16,7 @@ 'use strict'; -describe("ContactsBuffer", function() { +describe("ContactBuffer", function() { function getTestBuffer() { var buffer = new dcodeIO.ByteBuffer(); var avatarBuffer = new dcodeIO.ByteBuffer(); @@ -47,18 +47,72 @@ describe("ContactsBuffer", function() { it("parses an array buffer of contacts", function() { var arrayBuffer = getTestBuffer(); var contactBuffer = new ContactBuffer(arrayBuffer); - var contact = contactBuffer.readContact(); + var contact = contactBuffer.next(); var count = 0; while (contact !== undefined) { count++; assert.strictEqual(contact.name, "Zero Cool"); assert.strictEqual(contact.number, "+10000000000"); assert.strictEqual(contact.avatar.contentType, "image/jpg"); + assert.strictEqual(contact.avatar.length, 255); + assert.strictEqual(contact.avatar.data.byteLength, 255); var avatarBytes = new Uint8Array(contact.avatar.data); for (var j=0; j < 255; ++j) { assert.strictEqual(avatarBytes[j],j); } - contact = contactBuffer.readContact(); + contact = contactBuffer.next(); + } + assert.strictEqual(count, 3); + }); +}); + +describe("GroupBuffer", function() { + function getTestBuffer() { + var buffer = new dcodeIO.ByteBuffer(); + var avatarBuffer = new dcodeIO.ByteBuffer(); + var avatarLen = 255; + for (var i=0; i < avatarLen; ++i) { + avatarBuffer.writeUint8(i); + } + avatarBuffer.limit = avatarBuffer.offset; + avatarBuffer.offset = 0; + var groupInfo = new textsecure.protobuf.GroupDetails({ + id: new Uint8Array([1, 3, 3, 7]).buffer, + name: "Hackers", + members: ['cereal', 'burn', 'phreak', 'joey'], + avatar: { contentType: "image/jpg", length: avatarLen } + }); + var groupInfoBuffer = groupInfo.encode().toArrayBuffer(); + + for (var i = 0; i < 3; ++i) { + buffer.writeVarint32(groupInfoBuffer.byteLength); + buffer.append(groupInfoBuffer); + buffer.append(avatarBuffer.clone()); + } + + buffer.limit = buffer.offset; + buffer.offset = 0; + return buffer.toArrayBuffer(); + } + + it("parses an array buffer of groups", function() { + var arrayBuffer = getTestBuffer(); + var groupBuffer = new GroupBuffer(arrayBuffer); + var group = groupBuffer.next(); + var count = 0; + while (group !== undefined) { + count++; + assert.strictEqual(group.name, "Hackers"); + assertEqualArrayBuffers(group.id.toArrayBuffer(), new Uint8Array([1,3,3,7]).buffer); + assert.sameMembers(group.members, ['cereal', 'burn', 'phreak', 'joey']); + assert.strictEqual(group.avatar.contentType, "image/jpg"); + assert.strictEqual(group.avatar.length, 255); + assert.strictEqual(group.avatar.data.byteLength, 255); + var avatarBytes = new Uint8Array(group.avatar.data); + for (var j=0; j < 255; ++j) { + assert.strictEqual(avatarBytes[j],j); + } + group = groupBuffer.next(); } assert.strictEqual(count, 3); }); diff --git a/protos/IncomingPushMessageSignal.proto b/protos/IncomingPushMessageSignal.proto index 2baea196a73..e8a7b42c7d7 100644 --- a/protos/IncomingPushMessageSignal.proto +++ b/protos/IncomingPushMessageSignal.proto @@ -48,21 +48,22 @@ message SyncMessage { optional AttachmentPointer blob = 1; } - message Group { - optional GroupContext group = 1; + message Groups { + optional AttachmentPointer blob = 1; } message Request { enum Type { UNKNOWN = 0; CONTACTS = 1; + GROUPS = 2; } optional Type type = 1; } optional Sent sent = 1; optional Contacts contacts = 2; - optional Group group = 3; + optional Groups groups = 3; optional Request request = 4; } @@ -86,12 +87,19 @@ message GroupContext { optional AttachmentPointer avatar = 5; } -message ContactDetails { - message Avatar { - optional string contentType = 1; - optional uint64 length = 2; - } +message Avatar { + optional string contentType = 1; + optional uint32 length = 2; +} +message GroupDetails { + optional bytes id = 1; + optional string name = 2; + repeated string members = 3; + optional Avatar avatar = 4; +} + +message ContactDetails { optional string number = 1; optional string name = 2; optional Avatar avatar = 3;