diff --git a/app/sql.js b/app/sql.js index b49cb0bea0..ac26078fff 100644 --- a/app/sql.js +++ b/app/sql.js @@ -16,14 +16,6 @@ module.exports = { removeDB, removeIndexedDBFiles, - createOrUpdateGroup, - getGroupById, - getAllGroupIds, - getAllGroups, - bulkAddGroups, - removeGroupById, - removeAllGroups, - createOrUpdateIdentityKey, getIdentityKeyById, bulkAddIdentityKeys, @@ -625,6 +617,20 @@ async function updateToSchemaVersion10(currentVersion, instance) { console.log('updateToSchemaVersion10: success!'); } +async function updateToSchemaVersion11(currentVersion, instance) { + if (currentVersion >= 11) { + return; + } + console.log('updateToSchemaVersion11: starting...'); + await instance.run('BEGIN TRANSACTION;'); + + await instance.run('DROP TABLE groups;'); + + await instance.run('PRAGMA schema_version = 11;'); + await instance.run('COMMIT TRANSACTION;'); + console.log('updateToSchemaVersion11: success!'); +} + const SCHEMA_VERSIONS = [ updateToSchemaVersion1, updateToSchemaVersion2, @@ -636,6 +642,7 @@ const SCHEMA_VERSIONS = [ updateToSchemaVersion8, updateToSchemaVersion9, updateToSchemaVersion10, + updateToSchemaVersion11, ]; async function updateSchema(instance) { @@ -726,31 +733,6 @@ async function removeIndexedDBFiles() { indexedDBPath = null; } -const GROUPS_TABLE = 'groups'; -async function createOrUpdateGroup(data) { - return createOrUpdate(GROUPS_TABLE, data); -} -async function getGroupById(id) { - return getById(GROUPS_TABLE, id); -} -async function getAllGroupIds() { - const rows = await db.all('SELECT id FROM groups ORDER BY id ASC;'); - return map(rows, row => row.id); -} -async function getAllGroups() { - const rows = await db.all('SELECT json FROM groups ORDER BY id ASC;'); - return map(rows, row => jsonToObject(row.json)); -} -async function bulkAddGroups(array) { - return bulkAdd(GROUPS_TABLE, array); -} -async function removeGroupById(id) { - return removeById(GROUPS_TABLE, id); -} -async function removeAllGroups() { - return removeAllFromTable(GROUPS_TABLE); -} - const IDENTITY_KEYS_TABLE = 'identityKeys'; async function createOrUpdateIdentityKey(data) { return createOrUpdate(IDENTITY_KEYS_TABLE, data); @@ -1701,7 +1683,6 @@ async function removeAll() { promise = Promise.all([ db.run('BEGIN TRANSACTION;'), db.run('DELETE FROM conversations;'), - db.run('DELETE FROM groups;'), db.run('DELETE FROM identityKeys;'), db.run('DELETE FROM items;'), db.run('DELETE FROM messages;'), diff --git a/background.html b/background.html index c4df972c28..9d780a0f8e 100644 --- a/background.html +++ b/background.html @@ -615,7 +615,6 @@ - diff --git a/js/models/conversations.js b/js/models/conversations.js index 160be22a2d..048f304e26 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -210,14 +210,16 @@ sendTypingMessage(isTyping) { const groupId = !this.isPrivate() ? this.id : null; const recipientId = this.isPrivate() ? this.id : null; + const groupNumbers = this.getRecipients(); const sendOptions = this.getSendOptions(); this.wrapSend( textsecure.messaging.sendTypingMessage( { - groupId, isTyping, recipientId, + groupId, + groupNumbers, }, sendOptions ) @@ -929,12 +931,36 @@ } const conversationType = this.get('type'); - const sendFunction = (() => { + const options = this.getSendOptions(); + const groupNumbers = this.getRecipients(); + + const promise = (() => { switch (conversationType) { case Message.PRIVATE: - return textsecure.messaging.sendMessageToNumber; + return textsecure.messaging.sendMessageToNumber( + destination, + body, + attachmentsWithData, + quote, + preview, + now, + expireTimer, + profileKey, + options + ); case Message.GROUP: - return textsecure.messaging.sendMessageToGroup; + return textsecure.messaging.sendMessageToGroup( + destination, + groupNumbers, + body, + attachmentsWithData, + quote, + preview, + now, + expireTimer, + profileKey, + options + ); default: throw new TypeError( `Invalid conversation type: '${conversationType}'` @@ -942,22 +968,7 @@ } })(); - const options = this.getSendOptions(); - return message.send( - this.wrapSend( - sendFunction( - destination, - body, - attachmentsWithData, - quote, - preview, - now, - expireTimer, - profileKey, - options - ) - ) - ); + return message.send(this.wrapSend(promise)); }); }, @@ -1239,25 +1250,31 @@ return message; } - let sendFunc; - if (this.get('type') === 'private') { - sendFunc = textsecure.messaging.sendExpirationTimerUpdateToNumber; - } else { - sendFunc = textsecure.messaging.sendExpirationTimerUpdateToGroup; - } let profileKey; if (this.get('profileSharing')) { profileKey = storage.get('profileKey'); } - const sendOptions = this.getSendOptions(); - const promise = sendFunc( - this.get('id'), - this.get('expireTimer'), - message.get('sent_at'), - profileKey, - sendOptions - ); + let promise; + + if (this.get('type') === 'private') { + promise = textsecure.messaging.sendExpirationTimerUpdateToNumber( + this.get('id'), + this.get('expireTimer'), + message.get('sent_at'), + profileKey, + sendOptions + ); + } else { + promise = textsecure.messaging.sendExpirationTimerUpdateToGroup( + this.get('id'), + this.getRecipients(), + this.get('expireTimer'), + message.get('sent_at'), + profileKey, + sendOptions + ); + } await message.send(this.wrapSend(promise)); @@ -1335,6 +1352,7 @@ async leaveGroup() { const now = Date.now(); if (this.get('type') === 'group') { + const groupNumbers = this.getRecipients(); this.set({ left: true }); await window.Signal.Data.updateConversation(this.id, this.attributes, { Conversation: Whisper.Conversation, @@ -1355,7 +1373,9 @@ const options = this.getSendOptions(); message.send( - this.wrapSend(textsecure.messaging.leaveGroup(this.id, options)) + this.wrapSend( + textsecure.messaging.leaveGroup(this.id, groupNumbers, options) + ) ); } }, diff --git a/js/models/messages.js b/js/models/messages.js index 1da77e20a4..b674aa8e7a 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -706,21 +706,35 @@ this.isReplayableError.bind(this) ); - // Remove the errors that aren't replayable + // Put the errors back which aren't replayable this.set({ errors }); - const profileKey = null; - let numbers = retries + const conversation = this.getConversation(); + const intendedRecipients = this.get('recipients') || []; + const currentRecipients = conversation.getRecipients(); + + const profileKey = conversation.get('profileSharing') + ? storage.get('profileKey') + : null; + + const errorNumbers = retries .map(retry => retry.number) .filter(item => Boolean(item)); + let numbers = _.intersection( + errorNumbers, + intendedRecipients, + currentRecipients + ); if (!numbers.length) { window.log.warn( 'retrySend: No numbers in error set, using all recipients' ); - const conversation = this.getConversation(); + if (conversation) { - numbers = conversation.getRecipients(); + numbers = _.intersection(currentRecipients, intendedRecipients); + // We clear all errors here to start with a fresh slate, since we are starting + // from scratch on this message with a fresh set of recipients this.set({ errors: null }); } else { throw new Error( @@ -752,7 +766,6 @@ } let promise; - const conversation = this.getConversation(); const options = conversation.getSendOptions(); if (conversation.isPrivate()) { diff --git a/js/modules/backup.js b/js/modules/backup.js index b486a6e10d..f527a142ee 100644 --- a/js/modules/backup.js +++ b/js/modules/backup.js @@ -109,9 +109,9 @@ function createOutputStream(writer) { }; } -async function exportContactAndGroupsToFile(parent) { +async function exportConversationListToFile(parent) { const writer = await createFileAndWriter(parent, 'db.json'); - return exportContactsAndGroups(writer); + return exportConversationList(writer); } function writeArray(stream, array) { @@ -137,7 +137,7 @@ function getPlainJS(collection) { return collection.map(model => model.attributes); } -async function exportContactsAndGroups(fileWriter) { +async function exportConversationList(fileWriter) { const stream = createOutputStream(fileWriter); stream.write('{'); @@ -149,13 +149,6 @@ async function exportContactsAndGroups(fileWriter) { window.log.info(`Exporting ${conversations.length} conversations`); writeArray(stream, getPlainJS(conversations)); - stream.write(','); - - stream.write('"groups": '); - const groups = await window.Signal.Data.getAllGroups(); - window.log.info(`Exporting ${groups.length} groups`); - writeArray(stream, groups); - stream.write('}'); await stream.close(); } @@ -167,7 +160,7 @@ async function importNonMessages(parent, options) { } function eliminateClientConfigInBackup(data, targetPath) { - const cleaned = _.pick(data, 'conversations', 'groups'); + const cleaned = _.pick(data, 'conversations'); window.log.info('Writing configuration-free backup file back to disk'); try { fs.writeFileSync(targetPath, JSON.stringify(cleaned)); @@ -223,10 +216,8 @@ async function importFromJsonString(jsonString, targetPath, options) { _.defaults(options, { forceLightImport: false, conversationLookup: {}, - groupLookup: {}, }); - const { groupLookup } = options; const result = { fullImport: true, }; @@ -251,7 +242,7 @@ async function importFromJsonString(jsonString, targetPath, options) { // We mutate the on-disk backup to prevent the user from importing client // configuration more than once - that causes lots of encryption errors. - // This of course preserves the true data: conversations and groups. + // This of course preserves the true data: conversations. eliminateClientConfigInBackup(importObject, targetPath); const storeNames = _.keys(importObject); @@ -262,12 +253,12 @@ async function importFromJsonString(jsonString, targetPath, options) { const remainingStoreNames = _.without( storeNames, 'conversations', - 'unprocessed' + 'unprocessed', + 'groups' // in old data sets, but no longer included in database schema ); await importConversationsFromJSON(conversations, options); const SAVE_FUNCTIONS = { - groups: window.Signal.Data.createOrUpdateGroup, identityKeys: window.Signal.Data.createOrUpdateIdentityKey, items: window.Signal.Data.createOrUpdateItem, preKeys: window.Signal.Data.createOrUpdatePreKey, @@ -292,29 +283,17 @@ async function importFromJsonString(jsonString, targetPath, options) { return; } - let skipCount = 0; - for (let i = 0, max = toImport.length; i < max; i += 1) { const toAdd = unstringify(toImport[i]); - - const haveGroupAlready = - storeName === 'groups' && groupLookup[getGroupKey(toAdd)]; - - if (haveGroupAlready) { - skipCount += 1; - } else { - // eslint-disable-next-line no-await-in-loop - await save(toAdd); - } + // eslint-disable-next-line no-await-in-loop + await save(toAdd); } window.log.info( 'Done importing to store', storeName, 'Total count:', - toImport.length, - 'Skipped:', - skipCount + toImport.length ); }) ); @@ -1160,14 +1139,6 @@ async function loadConversationLookup() { return fromPairs(map(array, item => [getConversationKey(item), true])); } -function getGroupKey(group) { - return group.id; -} -async function loadGroupsLookup() { - const array = await window.Signal.Data.getAllGroupIds(); - return fromPairs(map(array, item => [getGroupKey(item), true])); -} - function getDirectoryForExport() { return getDirectory(); } @@ -1254,7 +1225,7 @@ async function exportToDirectory(directory, options) { const attachmentsDir = await createDirectory(directory, 'attachments'); - await exportContactAndGroupsToFile(stagingDir); + await exportConversationListToFile(stagingDir); await exportConversations( Object.assign({}, options, { messagesDir: stagingDir, @@ -1298,13 +1269,11 @@ async function importFromDirectory(directory, options) { const lookups = await Promise.all([ loadMessagesLookup(), loadConversationLookup(), - loadGroupsLookup(), ]); - const [messageLookup, conversationLookup, groupLookup] = lookups; + const [messageLookup, conversationLookup] = lookups; options = Object.assign({}, options, { messageLookup, conversationLookup, - groupLookup, }); const archivePath = path.join(directory, ARCHIVE_NAME); diff --git a/js/modules/data.js b/js/modules/data.js index 2b1c207fc7..8eb9250458 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -47,14 +47,6 @@ module.exports = { removeDB, removeIndexedDBFiles, - createOrUpdateGroup, - getGroupById, - getAllGroupIds, - getAllGroups, - bulkAddGroups, - removeGroupById, - removeAllGroups, - createOrUpdateIdentityKey, getIdentityKeyById, bulkAddIdentityKeys, @@ -395,33 +387,6 @@ async function removeIndexedDBFiles() { await channels.removeIndexedDBFiles(); } -// Groups - -async function createOrUpdateGroup(data) { - await channels.createOrUpdateGroup(data); -} -async function getGroupById(id) { - const group = await channels.getGroupById(id); - return group; -} -async function getAllGroupIds() { - const ids = await channels.getAllGroupIds(); - return ids; -} -async function getAllGroups() { - const groups = await channels.getAllGroups(); - return groups; -} -async function bulkAddGroups(array) { - await channels.bulkAddGroups(array); -} -async function removeGroupById(id) { - await channels.removeGroupById(id); -} -async function removeAllGroups() { - await channels.removeAllGroups(); -} - // Identity Keys const IDENTITY_KEY_KEYS = ['publicKey']; diff --git a/js/modules/migrate_to_sql.js b/js/modules/migrate_to_sql.js index 00dba4df49..4278ad0398 100644 --- a/js/modules/migrate_to_sql.js +++ b/js/modules/migrate_to_sql.js @@ -2,14 +2,12 @@ const { includes, isFunction, isString, last, map } = require('lodash'); const { - bulkAddGroups, bulkAddSessions, bulkAddIdentityKeys, bulkAddPreKeys, bulkAddSignedPreKeys, bulkAddItems, - removeGroupById, removeSessionById, removeIdentityKeyById, removePreKeyById, @@ -184,31 +182,6 @@ async function migrateToSQL({ complete = false; lastIndex = null; - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: bulkAddGroups, - remove: removeGroupById, - storeName: 'groups', - handleDOMException, - lastIndex, - batchSize: 10, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of groups complete'); - try { - await clearStores(['groups']); - } catch (error) { - window.log.warn('Failed to clear groups store'); - } - - complete = false; - lastIndex = null; - while (!complete) { // eslint-disable-next-line no-await-in-loop const status = await migrateStoreToSQLite({ diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index bf27c2850a..0a14b1a7cf 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -828,41 +828,6 @@ await textsecure.storage.protocol.removeAllSessions(number); }, - // Groups - - async getGroup(groupId) { - if (groupId === null || groupId === undefined) { - throw new Error('Tried to get group for undefined/null id'); - } - - const group = await window.Signal.Data.getGroupById(groupId); - if (group) { - return group.data; - } - - return undefined; - }, - async putGroup(groupId, group) { - if (groupId === null || groupId === undefined) { - throw new Error('Tried to put group key for undefined/null id'); - } - if (group === null || group === undefined) { - throw new Error('Tried to put undefined/null group object'); - } - const data = { - id: groupId, - data: group, - }; - await window.Signal.Data.createOrUpdateGroup(data); - }, - async removeGroup(groupId) { - if (groupId === null || groupId === undefined) { - throw new Error('Tried to remove group key for undefined/null id'); - } - - await window.Signal.Data.removeGroupById(groupId); - }, - // Not yet processed messages - for resiliency getUnprocessedCount() { return window.Signal.Data.getUnprocessedCount(); diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index cfa769aeb8..b509785cca 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -1135,16 +1135,7 @@ async showMembers(e, providedMembers, options = {}) { _.defaults(options, { needVerify: false }); - const fromConversation = this.model.isPrivate() - ? [this.model.id] - : await textsecure.storage.groups.getNumbers(this.model.id); - const members = - providedMembers || - fromConversation.map(id => ConversationController.get(id)); - - const model = this.model.getContactCollection(); - model.reset(members); - + const model = providedMembers || this.model.contactCollection; const view = new Whisper.GroupMemberList({ model, // we pass this in to allow nested panels diff --git a/js/views/new_group_update_view.js b/js/views/new_group_update_view.js deleted file mode 100644 index c15494e996..0000000000 --- a/js/views/new_group_update_view.js +++ /dev/null @@ -1,99 +0,0 @@ -/* global Whisper, _ */ - -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - Whisper.NewGroupUpdateView = Whisper.View.extend({ - tagName: 'div', - className: 'new-group-update', - templateName: 'new-group-update', - initialize(options) { - this.render(); - this.avatarInput = new Whisper.FileInputView({ - el: this.$('.group-avatar'), - window: options.window, - }); - - this.recipients_view = new Whisper.RecipientsInputView(); - this.listenTo(this.recipients_view.typeahead, 'sync', () => - this.model.contactCollection.models.forEach(model => { - if (this.recipients_view.typeahead.get(model)) { - this.recipients_view.typeahead.remove(model); - } - }) - ); - this.recipients_view.$el.insertBefore(this.$('.container')); - - this.member_list_view = new Whisper.ContactListView({ - collection: this.model.contactCollection, - className: 'members', - }); - this.member_list_view.render(); - this.$('.scrollable').append(this.member_list_view.el); - }, - events: { - 'click .back': 'goBack', - 'click .send': 'send', - 'focusin input.search': 'showResults', - 'focusout input.search': 'hideResults', - }, - hideResults() { - this.$('.results').hide(); - }, - showResults() { - this.$('.results').show(); - }, - goBack() { - this.trigger('back'); - }, - render_attributes() { - return { - name: this.model.getTitle(), - avatar: this.model.getAvatar(), - }; - }, - async send() { - // When we turn this view on again, need to handle avatars in the new way - - // const avatarFile = await this.avatarInput.getThumbnail(); - const now = Date.now(); - const attrs = { - timestamp: now, - active_at: now, - name: this.$('.name').val(), - members: _.union( - this.model.get('members'), - this.recipients_view.recipients.pluck('id') - ), - }; - - // if (avatarFile) { - // attrs.avatar = avatarFile; - // } - - // Because we're no longer using Backbone-integrated saves, we need to manually - // clear the changed fields here so model.changed is accurate. - this.model.changed = {}; - this.model.set(attrs); - const groupUpdate = this.model.changed; - - await window.Signal.Data.updateConversation( - this.model.id, - this.model.attributes, - { Conversation: Whisper.Conversation } - ); - - if (groupUpdate.avatar) { - this.model.trigger('change:avatar'); - } - - this.model.updateGroup(groupUpdate); - this.goBack(); - }, - }); -})(); diff --git a/js/views/recipients_input_view.js b/js/views/recipients_input_view.js deleted file mode 100644 index af5ea95578..0000000000 --- a/js/views/recipients_input_view.js +++ /dev/null @@ -1,183 +0,0 @@ -/* global Whisper, Backbone, ConversationController */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - const ContactsTypeahead = Backbone.TypeaheadCollection.extend({ - typeaheadAttributes: [ - 'name', - 'e164_number', - 'national_number', - 'international_number', - ], - model: Whisper.Conversation, - async fetchContacts() { - const models = window.Signal.Data.getAllPrivateConversations({ - ConversationCollection: Whisper.ConversationCollection, - }); - - this.reset(models); - }, - }); - - Whisper.ContactPillView = Whisper.View.extend({ - tagName: 'span', - className: 'recipient', - events: { - 'click .remove': 'removeModel', - }, - templateName: 'contact_pill', - initialize() { - const error = this.model.validate(this.model.attributes); - if (error) { - this.$el.addClass('error'); - } - }, - removeModel() { - this.$el.trigger('remove', { modelId: this.model.id }); - this.remove(); - }, - render_attributes() { - return { name: this.model.getTitle() }; - }, - }); - - Whisper.RecipientListView = Whisper.ListView.extend({ - itemView: Whisper.ContactPillView, - }); - - Whisper.SuggestionView = Whisper.ConversationListItemView.extend({ - className: 'contact-details contact', - templateName: 'contact_name_and_number', - }); - - Whisper.SuggestionListView = Whisper.ConversationListView.extend({ - itemView: Whisper.SuggestionView, - }); - - Whisper.RecipientsInputView = Whisper.View.extend({ - className: 'recipients-input', - templateName: 'recipients-input', - initialize(options) { - if (options) { - this.placeholder = options.placeholder; - } - this.render(); - this.$input = this.$('input.search'); - this.$new_contact = this.$('.new-contact'); - - // Collection of recipients selected for the new message - this.recipients = new Whisper.ConversationCollection([], { - comparator: false, - }); - - // View to display the selected recipients - this.recipients_view = new Whisper.RecipientListView({ - collection: this.recipients, - el: this.$('.recipients'), - }); - - // Collection of contacts to match user input against - this.typeahead = new ContactsTypeahead(); - this.typeahead.fetchContacts(); - - // View to display the matched contacts from typeahead - this.typeahead_view = new Whisper.SuggestionListView({ - collection: new Whisper.ConversationCollection([], { - comparator(m) { - return m.getTitle().toLowerCase(); - }, - }), - }); - this.$('.contacts').append(this.typeahead_view.el); - this.initNewContact(); - this.listenTo(this.typeahead, 'reset', this.filterContacts); - }, - - render_attributes() { - return { placeholder: this.placeholder || 'name or phone number' }; - }, - - events: { - 'input input.search': 'filterContacts', - 'select .new-contact': 'addNewRecipient', - 'select .contacts': 'addRecipient', - 'remove .recipient': 'removeRecipient', - }, - - filterContacts() { - const query = this.$input.val(); - if (query.length) { - if (this.maybeNumber(query)) { - this.new_contact_view.model.set('id', query); - this.new_contact_view.render().$el.show(); - } else { - this.new_contact_view.$el.hide(); - } - this.typeahead_view.collection.reset(this.typeahead.typeahead(query)); - } else { - this.resetTypeahead(); - } - }, - - initNewContact() { - if (this.new_contact_view) { - this.new_contact_view.undelegateEvents(); - this.new_contact_view.$el.hide(); - } - // Creates a view to display a new contact - this.new_contact_view = new Whisper.ConversationListItemView({ - el: this.$new_contact, - model: ConversationController.create({ - type: 'private', - newContact: true, - }), - }).render(); - }, - - addNewRecipient() { - this.recipients.add(this.new_contact_view.model); - this.initNewContact(); - this.resetTypeahead(); - }, - - addRecipient(e, conversation) { - this.recipients.add(this.typeahead.remove(conversation.id)); - this.resetTypeahead(); - }, - - removeRecipient(e, data) { - const model = this.recipients.remove(data.modelId); - if (!model.get('newContact')) { - this.typeahead.add(model); - } - this.filterContacts(); - }, - - reset() { - this.delegateEvents(); - this.typeahead_view.delegateEvents(); - this.recipients_view.delegateEvents(); - this.new_contact_view.delegateEvents(); - this.typeahead.add( - this.recipients.filter(model => !model.get('newContact')) - ); - this.recipients.reset([]); - this.resetTypeahead(); - this.typeahead.fetchContacts(); - }, - - resetTypeahead() { - this.new_contact_view.$el.hide(); - this.$input.val('').focus(); - this.typeahead_view.collection.reset([]); - }, - - maybeNumber(number) { - return number.match(/^\+?[0-9]*$/); - }, - }); -})(); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index c417434837..a8a7f23bae 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -229,6 +229,8 @@ MessageReceiver.prototype.extend({ const job = () => appJobPromise; this.appPromise = promise.then(job, job); + + return Promise.resolve(); }, onclose(ev) { window.log.info( @@ -868,7 +870,7 @@ MessageReceiver.prototype.extend({ p = this.handleEndSession(destination); } return p.then(() => - this.processDecrypted(envelope, msg, this.number).then(message => { + this.processDecrypted(envelope, msg).then(message => { const groupId = message.group && message.group.id; const isBlocked = this.isGroupBlocked(groupId); const isMe = envelope.source === textsecure.storage.user.getNumber(); @@ -910,7 +912,7 @@ MessageReceiver.prototype.extend({ p = this.handleEndSession(envelope.source); } return p.then(() => - this.processDecrypted(envelope, msg, envelope.source).then(message => { + this.processDecrypted(envelope, msg).then(message => { const groupId = message.group && message.group.id; const isBlocked = this.isGroupBlocked(groupId); const isMe = envelope.source === textsecure.storage.user.getNumber(); @@ -1168,39 +1170,13 @@ MessageReceiver.prototype.extend({ let groupDetails = groupBuffer.next(); const promises = []; while (groupDetails !== undefined) { - const getGroupDetails = details => { - // eslint-disable-next-line no-param-reassign - details.id = details.id.toBinary(); - if (details.active) { - return textsecure.storage.groups - .getGroup(details.id) - .then(existingGroup => { - if (existingGroup === undefined) { - return textsecure.storage.groups.createNewGroup( - details.members, - details.id - ); - } - return textsecure.storage.groups.updateNumbers( - details.id, - details.members - ); - }) - .then(() => details); - } - return Promise.resolve(details); - }; - - const promise = getGroupDetails(groupDetails) - .then(details => { - const ev = new Event('group'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.groupDetails = details; - return this.dispatchAndWait(ev); - }) - .catch(e => { - window.log.error('error processing group', e); - }); + groupDetails.id = groupDetails.id.toBinary(); + const ev = new Event('group'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.groupDetails = groupDetails; + const promise = this.dispatchAndWait(ev).catch(e => { + window.log.error('error processing group', e); + }); groupDetails = groupBuffer.next(); promises.push(promise); } @@ -1275,7 +1251,7 @@ MessageReceiver.prototype.extend({ }) ); }, - processDecrypted(envelope, decrypted, source) { + processDecrypted(envelope, decrypted) { /* eslint-disable no-bitwise, no-param-reassign */ const FLAGS = textsecure.protobuf.DataMessage.Flags; @@ -1311,63 +1287,24 @@ MessageReceiver.prototype.extend({ if (decrypted.group !== null) { decrypted.group.id = decrypted.group.id.toBinary(); - const storageGroups = textsecure.storage.groups; - - promises.push( - storageGroups.getNumbers(decrypted.group.id).then(existingGroup => { - if (existingGroup === undefined) { - if ( - decrypted.group.type !== - textsecure.protobuf.GroupContext.Type.UPDATE - ) { - decrypted.group.members = [source]; - window.log.warn('Got message for unknown group'); - } - return textsecure.storage.groups.createNewGroup( - decrypted.group.members, - decrypted.group.id - ); - } - const fromIndex = existingGroup.indexOf(source); - - if (fromIndex < 0) { - // TODO: This could be indication of a race... - window.log.warn( - 'Sender was not a member of the group they were sending from' - ); - } - - switch (decrypted.group.type) { - case textsecure.protobuf.GroupContext.Type.UPDATE: - decrypted.body = null; - decrypted.attachments = []; - return textsecure.storage.groups.updateNumbers( - decrypted.group.id, - decrypted.group.members - ); - case textsecure.protobuf.GroupContext.Type.QUIT: - decrypted.body = null; - decrypted.attachments = []; - if (source === this.number) { - return textsecure.storage.groups.deleteGroup( - decrypted.group.id - ); - } - return textsecure.storage.groups.removeNumber( - decrypted.group.id, - source - ); - case textsecure.protobuf.GroupContext.Type.DELIVER: - decrypted.group.name = null; - decrypted.group.members = []; - decrypted.group.avatar = null; - return Promise.resolve(); - default: - this.removeFromCache(envelope); - throw new Error('Unknown group message type'); - } - }) - ); + switch (decrypted.group.type) { + case textsecure.protobuf.GroupContext.Type.UPDATE: + decrypted.body = null; + decrypted.attachments = []; + break; + case textsecure.protobuf.GroupContext.Type.QUIT: + decrypted.body = null; + decrypted.attachments = []; + break; + case textsecure.protobuf.GroupContext.Type.DELIVER: + decrypted.group.name = null; + decrypted.group.members = []; + decrypted.group.avatar = null; + break; + default: + this.removeFromCache(envelope); + throw new Error('Unknown group message type'); + } } const attachmentCount = decrypted.attachments.length; diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 08ade90367..5357d1d73a 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -560,7 +560,7 @@ MessageSender.prototype = { async sendTypingMessage(options = {}, sendOptions = {}) { const ACTION_ENUM = textsecure.protobuf.TypingMessage.Action; - const { recipientId, groupId, isTyping, timestamp } = options; + const { recipientId, groupId, groupNumbers, isTyping, timestamp } = options; // We don't want to send typing messages to our other devices, but we will // in the group case. @@ -574,7 +574,7 @@ MessageSender.prototype = { } const recipients = groupId - ? _.without(await textsecure.storage.groups.getNumbers(groupId), myNumber) + ? _.without(groupNumbers, myNumber) : [recipientId]; const groupIdBuffer = groupId ? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(groupId) @@ -882,6 +882,7 @@ MessageSender.prototype = { sendMessageToGroup( groupId, + groupNumbers, messageText, attachments, quote, @@ -891,59 +892,50 @@ MessageSender.prototype = { profileKey, options ) { - return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => { - if (targetNumbers === undefined) { - return Promise.reject(new Error('Unknown Group')); - } + const me = textsecure.storage.user.getNumber(); + const numbers = groupNumbers.filter(number => number !== me); + if (numbers.length === 0) { + return Promise.reject(new Error('No other members in the group')); + } - const me = textsecure.storage.user.getNumber(); - const numbers = targetNumbers.filter(number => number !== me); - if (numbers.length === 0) { - return Promise.reject(new Error('No other members in the group')); - } - - return this.sendMessage( - { - recipients: numbers, - body: messageText, - timestamp, - attachments, - quote, - preview, - needsSync: true, - expireTimer, - profileKey, - group: { - id: groupId, - type: textsecure.protobuf.GroupContext.Type.DELIVER, - }, + return this.sendMessage( + { + recipients: numbers, + body: messageText, + timestamp, + attachments, + quote, + preview, + needsSync: true, + expireTimer, + profileKey, + group: { + id: groupId, + type: textsecure.protobuf.GroupContext.Type.DELIVER, }, - options - ); - }); + }, + options + ); }, - createGroup(targetNumbers, name, avatar, options) { + createGroup(targetNumbers, id, name, avatar, options) { const proto = new textsecure.protobuf.DataMessage(); proto.group = new textsecure.protobuf.GroupContext(); + proto.group.id = stringToArrayBuffer(id); - return textsecure.storage.groups - .createNewGroup(targetNumbers) - .then(group => { - proto.group.id = stringToArrayBuffer(group.id); - const { numbers } = group; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; + proto.group.members = targetNumbers; + proto.group.name = name; - proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; - proto.group.members = numbers; - proto.group.name = name; - - return this.makeAttachmentPointer(avatar).then(attachment => { - proto.group.avatar = attachment; - return this.sendGroupProto(numbers, proto, Date.now(), options).then( - () => proto.group.id - ); - }); - }); + return this.makeAttachmentPointer(avatar).then(attachment => { + proto.group.avatar = attachment; + return this.sendGroupProto( + targetNumbers, + proto, + Date.now(), + options + ).then(() => proto.group.id); + }); }, updateGroup(groupId, name, avatar, targetNumbers, options) { @@ -953,121 +945,87 @@ MessageSender.prototype = { proto.group.id = stringToArrayBuffer(groupId); proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.name = name; + proto.group.members = targetNumbers; - return textsecure.storage.groups - .addNumbers(groupId, targetNumbers) - .then(numbers => { - if (numbers === undefined) { - return Promise.reject(new Error('Unknown Group')); - } - proto.group.members = numbers; - - return this.makeAttachmentPointer(avatar).then(attachment => { - proto.group.avatar = attachment; - return this.sendGroupProto(numbers, proto, Date.now(), options).then( - () => proto.group.id - ); - }); - }); + return this.makeAttachmentPointer(avatar).then(attachment => { + proto.group.avatar = attachment; + return this.sendGroupProto( + targetNumbers, + proto, + Date.now(), + options + ).then(() => proto.group.id); + }); }, - addNumberToGroup(groupId, number, options) { + addNumberToGroup(groupId, newNumbers, options) { const proto = new textsecure.protobuf.DataMessage(); proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = stringToArrayBuffer(groupId); proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; - - return textsecure.storage.groups - .addNumbers(groupId, [number]) - .then(numbers => { - if (numbers === undefined) - return Promise.reject(new Error('Unknown Group')); - proto.group.members = numbers; - - return this.sendGroupProto(numbers, proto, Date.now(), options); - }); + proto.group.members = newNumbers; + return this.sendGroupProto(newNumbers, proto, Date.now(), options); }, - setGroupName(groupId, name, options) { + setGroupName(groupId, name, groupNumbers, options) { const proto = new textsecure.protobuf.DataMessage(); proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = stringToArrayBuffer(groupId); proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.name = name; + proto.group.members = groupNumbers; - return textsecure.storage.groups.getNumbers(groupId).then(numbers => { - if (numbers === undefined) - return Promise.reject(new Error('Unknown Group')); - proto.group.members = numbers; - - return this.sendGroupProto(numbers, proto, Date.now(), options); - }); + return this.sendGroupProto(groupNumbers, proto, Date.now(), options); }, - setGroupAvatar(groupId, avatar, options) { + setGroupAvatar(groupId, avatar, groupNumbers, options) { const proto = new textsecure.protobuf.DataMessage(); proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = stringToArrayBuffer(groupId); proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; + proto.group.members = groupNumbers; - return textsecure.storage.groups.getNumbers(groupId).then(numbers => { - if (numbers === undefined) - return Promise.reject(new Error('Unknown Group')); - proto.group.members = numbers; - - return this.makeAttachmentPointer(avatar).then(attachment => { - proto.group.avatar = attachment; - return this.sendGroupProto(numbers, proto, Date.now(), options); - }); + return this.makeAttachmentPointer(avatar).then(attachment => { + proto.group.avatar = attachment; + return this.sendGroupProto(groupNumbers, proto, Date.now(), options); }); }, - leaveGroup(groupId, options) { + leaveGroup(groupId, groupNumbers, options) { const proto = new textsecure.protobuf.DataMessage(); proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = stringToArrayBuffer(groupId); proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT; - - return textsecure.storage.groups.getNumbers(groupId).then(numbers => { - if (numbers === undefined) - return Promise.reject(new Error('Unknown Group')); - return textsecure.storage.groups - .deleteGroup(groupId) - .then(() => this.sendGroupProto(numbers, proto, Date.now(), options)); - }); + return this.sendGroupProto(groupNumbers, proto, Date.now(), options); }, sendExpirationTimerUpdateToGroup( groupId, + groupNumbers, expireTimer, timestamp, profileKey, options ) { - return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => { - if (targetNumbers === undefined) - return Promise.reject(new Error('Unknown Group')); - - const me = textsecure.storage.user.getNumber(); - const numbers = targetNumbers.filter(number => number !== me); - if (numbers.length === 0) { - return Promise.reject(new Error('No other members in the group')); - } - return this.sendMessage( - { - recipients: numbers, - timestamp, - needsSync: true, - expireTimer, - profileKey, - flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, - group: { - id: groupId, - type: textsecure.protobuf.GroupContext.Type.DELIVER, - }, + const me = textsecure.storage.user.getNumber(); + const numbers = groupNumbers.filter(number => number !== me); + if (numbers.length === 0) { + return Promise.reject(new Error('No other members in the group')); + } + return this.sendMessage( + { + recipients: numbers, + timestamp, + needsSync: true, + expireTimer, + profileKey, + flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, + group: { + id: groupId, + type: textsecure.protobuf.GroupContext.Type.DELIVER, }, - options - ); - }); + }, + options + ); }, sendExpirationTimerUpdateToNumber( number, diff --git a/libtextsecure/storage/groups.js b/libtextsecure/storage/groups.js deleted file mode 100644 index 67460c15e2..0000000000 --- a/libtextsecure/storage/groups.js +++ /dev/null @@ -1,160 +0,0 @@ -/* global window, getString, libsignal, textsecure */ - -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function() { - /** ******************* - *** Group Storage *** - ******************** */ - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; - - // create a random group id that we haven't seen before. - function generateNewGroupId() { - const groupId = getString(libsignal.crypto.getRandomBytes(16)); - return textsecure.storage.protocol.getGroup(groupId).then(group => { - if (group === undefined) { - return groupId; - } - window.log.warn('group id collision'); // probably a bad sign. - return generateNewGroupId(); - }); - } - - window.textsecure.storage.groups = { - createNewGroup(numbers, groupId) { - return new Promise(resolve => { - if (groupId !== undefined) { - resolve( - textsecure.storage.protocol.getGroup(groupId).then(group => { - if (group !== undefined) { - throw new Error('Tried to recreate group'); - } - }) - ); - } else { - resolve( - generateNewGroupId().then(newGroupId => { - // eslint-disable-next-line no-param-reassign - groupId = newGroupId; - }) - ); - } - }).then(() => { - const me = textsecure.storage.user.getNumber(); - let haveMe = false; - const finalNumbers = []; - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const i in numbers) { - const number = numbers[i]; - if (!textsecure.utils.isNumberSane(number)) - throw new Error('Invalid number in group'); - if (number === me) haveMe = true; - if (finalNumbers.indexOf(number) < 0) finalNumbers.push(number); - } - - if (!haveMe) finalNumbers.push(me); - - const groupObject = { - numbers: finalNumbers, - numberRegistrationIds: {}, - }; - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const i in finalNumbers) { - groupObject.numberRegistrationIds[finalNumbers[i]] = {}; - } - - return textsecure.storage.protocol - .putGroup(groupId, groupObject) - .then(() => ({ id: groupId, numbers: finalNumbers })); - }); - }, - - getNumbers(groupId) { - return textsecure.storage.protocol.getGroup(groupId).then(group => { - if (!group) { - return undefined; - } - - return group.numbers; - }); - }, - - removeNumber(groupId, number) { - return textsecure.storage.protocol.getGroup(groupId).then(group => { - if (group === undefined) return undefined; - - const me = textsecure.storage.user.getNumber(); - if (number === me) - throw new Error( - 'Cannot remove ourselves from a group, leave the group instead' - ); - - const i = group.numbers.indexOf(number); - if (i > -1) { - group.numbers.splice(i, 1); - // eslint-disable-next-line no-param-reassign - delete group.numberRegistrationIds[number]; - return textsecure.storage.protocol - .putGroup(groupId, group) - .then(() => group.numbers); - } - - return group.numbers; - }); - }, - - addNumbers(groupId, numbers) { - return textsecure.storage.protocol.getGroup(groupId).then(group => { - if (group === undefined) return undefined; - - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const i in numbers) { - const number = numbers[i]; - if (!textsecure.utils.isNumberSane(number)) - throw new Error('Invalid number in set to add to group'); - if (group.numbers.indexOf(number) < 0) { - group.numbers.push(number); - // eslint-disable-next-line no-param-reassign - group.numberRegistrationIds[number] = {}; - } - } - - return textsecure.storage.protocol - .putGroup(groupId, group) - .then(() => group.numbers); - }); - }, - - deleteGroup(groupId) { - return textsecure.storage.protocol.removeGroup(groupId); - }, - - getGroup(groupId) { - return textsecure.storage.protocol.getGroup(groupId).then(group => { - if (group === undefined) return undefined; - - return { id: groupId, numbers: group.numbers }; - }); - }, - - updateNumbers(groupId, numbers) { - return textsecure.storage.protocol.getGroup(groupId).then(group => { - if (group === undefined) - throw new Error('Tried to update numbers for unknown group'); - - if ( - numbers.filter(textsecure.utils.isNumberSane).length < numbers.length - ) - throw new Error('Invalid number in new group members'); - - const added = numbers.filter( - number => group.numbers.indexOf(number) < 0 - ); - - return textsecure.storage.groups.addNumbers(groupId, added); - }); - }, - }; -})(); diff --git a/test/index.html b/test/index.html index 64964f8962..8d64375df2 100644 --- a/test/index.html +++ b/test/index.html @@ -362,8 +362,6 @@ - - @@ -386,7 +384,6 @@ - diff --git a/test/views/group_update_view_test.js b/test/views/group_update_view_test.js deleted file mode 100644 index 45c9a7440c..0000000000 --- a/test/views/group_update_view_test.js +++ /dev/null @@ -1,24 +0,0 @@ -/* global Whisper */ - -describe('GroupUpdateView', () => { - it('should show new group members', () => { - const view = new Whisper.GroupUpdateView({ - model: { joined: ['Alice', 'Bob'] }, - }).render(); - assert.match(view.$el.text(), /Alice.*Bob.*joined the group/); - }); - - it('should note updates to the title', () => { - const view = new Whisper.GroupUpdateView({ - model: { name: 'New name' }, - }).render(); - assert.match(view.$el.text(), /Title is now 'New name'/); - }); - - it('should say "Updated the group"', () => { - const view = new Whisper.GroupUpdateView({ - model: { avatar: 'New avatar' }, - }).render(); - assert.match(view.$el.text(), /Updated the group/); - }); -});