From aa75205bbd9d4a4af60308be38efb4b38bbc719d Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 7 Jul 2020 09:13:55 +1000 Subject: [PATCH] Move BlockedNumberController to ts --- _locales/en/messages.json | 3 + background.html | 1 - background_test.html | 1 - js/background.js | 2 +- js/blocked_number_controller.js | 68 ----------- js/conversation_controller.js | 7 +- js/models/conversations.js | 36 +++++- js/signal_protocol_store.js | 2 +- js/views/blocked_number_view.js | 102 ---------------- js/views/conversation_view.js | 3 + preload.js | 12 +- test/blocked_number_controller_test.js | 163 ------------------------- test/index.html | 2 - ts/receiver/contentMessage.ts | 5 +- ts/receiver/groups.ts | 5 +- ts/util/blockedNumberController.ts | 119 ++++++++++++++++++ ts/util/index.ts | 2 + 17 files changed, 180 insertions(+), 353 deletions(-) delete mode 100644 js/blocked_number_controller.js delete mode 100644 js/views/blocked_number_view.js delete mode 100644 test/blocked_number_controller_test.js create mode 100644 ts/util/blockedNumberController.ts diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 522bc76f4..3fff6aa1b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1000,6 +1000,9 @@ "sendMessageLeftGroup": { "message": "You left this group" }, + "sendMessageBlockedUser": { + "message": "You have blocked the user" + }, "groupMembers": { "message": "Group members" }, diff --git a/background.html b/background.html index e97a9f00e..ad3063e11 100644 --- a/background.html +++ b/background.html @@ -463,7 +463,6 @@ - diff --git a/background_test.html b/background_test.html index de4dac8ae..cbb09488c 100644 --- a/background_test.html +++ b/background_test.html @@ -463,7 +463,6 @@ - diff --git a/js/background.js b/js/background.js index 89f5d242f..269cc5234 100644 --- a/js/background.js +++ b/js/background.js @@ -430,8 +430,8 @@ await Promise.all([ ConversationController.load(), textsecure.storage.protocol.hydrateCaches(), + BlockedNumberController.load(), ]); - BlockedNumberController.refresh(); } catch (error) { window.log.error( 'background.js: ConversationController failed to load:', diff --git a/js/blocked_number_controller.js b/js/blocked_number_controller.js deleted file mode 100644 index 7e1a30ab6..000000000 --- a/js/blocked_number_controller.js +++ /dev/null @@ -1,68 +0,0 @@ -/* global , Whisper, storage */ -/* global textsecure: false */ - -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - const blockedNumbers = new Whisper.BlockedNumberCollection(); - window.getBlockedNumbers = () => blockedNumbers; - - window.BlockedNumberController = { - reset() { - this.unblockAll(); - blockedNumbers.reset([]); - }, - refresh() { - window.log.info('BlockedNumberController: starting initial fetch'); - - if (!storage) { - throw new Error( - 'BlockedNumberController: Could not load blocked numbers' - ); - } - - // Add the numbers to the collection - const numbers = storage.getBlockedNumbers(); - blockedNumbers.reset(numbers.map(number => ({ number }))); - }, - block(number) { - const ourNumber = textsecure.storage.user.getNumber(); - - // Make sure we don't block ourselves - if (ourNumber === number) { - window.log.info('BlockedNumberController: Cannot block yourself!'); - return; - } - - storage.addBlockedNumber(number); - - // Make sure we don't add duplicates - if (blockedNumbers.getModel(number)) { - return; - } - - blockedNumbers.add({ number }); - }, - unblock(number) { - storage.removeBlockedNumber(number); - - // Remove the model from our collection - const model = blockedNumbers.getModel(number); - if (model) { - blockedNumbers.remove(model); - } - }, - unblockAll() { - const numbers = blockedNumbers.map(m => m.get('number')); - numbers.forEach(n => this.unblock(n)); - }, - isBlocked(number) { - return storage.isBlocked(number); - }, - }; -})(); diff --git a/js/conversation_controller.js b/js/conversation_controller.js index d4d938d4e..20512589a 100644 --- a/js/conversation_controller.js +++ b/js/conversation_controller.js @@ -195,7 +195,12 @@ }, getOrCreateAndWait(id, type) { return this._initialPromise.then(() => { - const pubkey = id.key ? id.key : id; + if (!id) { + return Promise.reject( + new Error('getOrCreateAndWait: invalid id passed.') + ); + } + const pubkey = id && id.key ? id.key : id; const conversation = this.getOrCreate(pubkey, type); if (conversation) { diff --git a/js/models/conversations.js b/js/models/conversations.js index 4beb66bf6..b98d92c02 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -224,20 +224,41 @@ return !!(this.id && this.id.match(/^rss:/)); }, isBlocked() { - return BlockedNumberController.isBlocked(this.id); + if (!this.id || this.isPublic() || this.isRss()) { + return false; + } + + return this.isPrivate() + ? BlockedNumberController.isBlocked(this.id) + : BlockedNumberController.isGroupBlocked(this.id); }, isMediumGroup() { return this.get('is_medium_group'); }, - block() { - BlockedNumberController.block(this.id); + async block() { + if (!this.id || this.isPublic() || this.isRss()) { + return; + } + + const promise = this.isPrivate() + ? BlockedNumberController.block(this.id) + : BlockedNumberController.blockGroup(this.id); + await promise; this.trigger('change'); this.messageCollection.forEach(m => m.trigger('change')); + this.updateTextInputState(); }, - unblock() { - BlockedNumberController.unblock(this.id); + async unblock() { + if (!this.id || this.isPublic() || this.isRss()) { + return; + } + const promise = this.isPrivate() + ? BlockedNumberController.unblock(this.id) + : BlockedNumberController.unblockGroup(this.id); + await promise; this.trigger('change'); this.messageCollection.forEach(m => m.trigger('change')); + this.updateTextInputState(); }, setMessageSelectionBackdrop() { const messageSelected = this.selectedMessages.size > 0; @@ -773,6 +794,11 @@ this.trigger('change:placeholder', 'left-group'); return; } + if (this.isBlocked()) { + this.trigger('disable:input', true); + this.trigger('change:placeholder', 'blocked-user'); + return; + } // otherwise, enable the input and set default placeholder this.trigger('disable:input', false); this.trigger('change:placeholder', 'chat'); diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index 3aa5a9aad..d5b77b9d9 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -898,7 +898,7 @@ ConversationController.reset(); BlockedNumberController.reset(); await ConversationController.load(); - BlockedNumberController.refresh(); + await BlockedNumberController.load(); }, async removeAllConfiguration() { await window.Signal.Data.removeAllConfiguration(); diff --git a/js/views/blocked_number_view.js b/js/views/blocked_number_view.js deleted file mode 100644 index cc14c1c48..000000000 --- a/js/views/blocked_number_view.js +++ /dev/null @@ -1,102 +0,0 @@ -/* global BlockedNumberController: false */ -/* global getBlockedNumbers: false */ -/* global Whisper: false */ -/* global storage: false */ -/* global i18n: false */ - -/* eslint-disable no-new */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - Whisper.BlockedNumberView = Whisper.View.extend({ - templateName: 'blockedUserSettings', - className: 'blockedUserSettings', - events: { - 'click .unblock-button': 'onUnblock', - }, - initialize() { - storage.onready(() => { - BlockedNumberController.refresh(); - this.collection = getBlockedNumbers(); - this.listView = new Whisper.BlockedNumberListView({ - collection: this.collection, - }); - - this.listView.render(); - this.blockedUserSettings = this.$('.blocked-user-settings'); - this.blockedUserSettings.prepend(this.listView.el); - }); - }, - render_attributes() { - return { - blockedHeader: i18n('settingsUnblockHeader'), - unblockMessage: i18n('unblockUser'), - }; - }, - onUnblock() { - const number = this.$('select option:selected').val(); - if (!number) { - return; - } - - if (BlockedNumberController.isBlocked(number)) { - BlockedNumberController.unblock(number); - window.onUnblockNumber(number); - this.listView.collection.remove( - this.listView.collection.where({ number }) - ); - } - }, - }); - - Whisper.BlockedNumberListView = Whisper.View.extend({ - tagName: 'select', - initialize(options) { - this.options = options || {}; - this.listenTo(this.collection, 'add', this.addOne); - this.listenTo(this.collection, 'reset', this.addAll); - this.listenTo(this.collection, 'remove', this.addAll); - }, - addOne(model) { - const number = model.get('number'); - if (number) { - this.$el.append( - `` - ); - } - }, - addAll() { - this.$el.html(''); - this.collection.each(this.addOne, this); - }, - truncate(string, limit) { - // Make sure an element and number of items to truncate is provided - if (!string || !limit) { - return string; - } - - // Get the inner content of the element - let content = string.trim(); - - // Convert the content into an array of words - // Remove any words above the limit - content = content.slice(0, limit); - - // Convert the array of words back into a string - // If there's content to add after it, add it - if (string.length > limit) { - content = `${content}...`; - } - - return content; - }, - render() { - this.addAll(); - return this; - }, - }); -})(); diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index a7010412c..21eff707c 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -535,6 +535,9 @@ case 'left-group': placeholder = i18n('sendMessageLeftGroup'); break; + case 'blocked-user': + placeholder = i18n('sendMessageBlockedUser'); + break; default: placeholder = i18n('sendMessage'); break; diff --git a/preload.js b/preload.js index 15ad38c5e..c0bbf3546 100644 --- a/preload.js +++ b/preload.js @@ -194,7 +194,7 @@ window.resetDatabase = () => { ipc.send('resetDatabase'); }; -window.onUnblockNumber = number => { +window.onUnblockNumber = async number => { // Unblock the number if (window.BlockedNumberController) { window.BlockedNumberController.unblock(number); @@ -204,7 +204,7 @@ window.onUnblockNumber = number => { if (window.ConversationController) { try { const conversation = window.ConversationController.get(number); - conversation.unblock(); + await conversation.unblock(); } catch (e) { window.log.info( 'IPC on unblock: failed to fetch conversation for number: ', @@ -509,3 +509,11 @@ if (config.environment.includes('test-integration')) { enableSenderKeys: true, }; } + +// Blocking + +const { + BlockedNumberController, +} = require('./ts/util/blockedNumberController'); + +window.BlockedNumberController = BlockedNumberController; diff --git a/test/blocked_number_controller_test.js b/test/blocked_number_controller_test.js deleted file mode 100644 index 1c904065f..000000000 --- a/test/blocked_number_controller_test.js +++ /dev/null @@ -1,163 +0,0 @@ -/* global textsecure, BlockedNumberController, storage */ - -'use strict'; - -describe('Blocked Number Controller', () => { - beforeEach(async () => { - // Purge everything manually - const numbers = storage.getBlockedNumbers(); - numbers.forEach(storage.removeBlockedNumber); - window.getBlockedNumbers().reset([]); - }); - - describe('reset', () => { - it('clears blocked numbers', () => { - BlockedNumberController.block('1'); - assert.isNotEmpty(storage.getBlockedNumbers()); - assert.isNotEmpty(window.getBlockedNumbers().models); - - BlockedNumberController.reset(); - assert.isEmpty(storage.getBlockedNumbers()); - assert.isEmpty(window.getBlockedNumbers().models); - }); - }); - - describe('refresh', () => { - it('loads blocked numbers from storage', () => { - BlockedNumberController.refresh(); - assert.isEmpty(window.getBlockedNumbers().models); - - storage.addBlockedNumber('1'); - storage.addBlockedNumber('2'); - BlockedNumberController.refresh(); - - const blocked = window.getBlockedNumbers().map(m => m.get('number')); - assert.lengthOf(blocked, 2); - assert.deepEqual(['1', '2'], blocked.sort()); - }); - - it('overrides old numbers if we refresh again', () => { - BlockedNumberController.refresh(); - assert.isEmpty(window.getBlockedNumbers().models); - - storage.addBlockedNumber('1'); - BlockedNumberController.refresh(); - assert.isNotEmpty( - window.getBlockedNumbers().find(m => m.get('number') === '1') - ); - - storage.removeBlockedNumber('1'); - storage.addBlockedNumber('2'); - BlockedNumberController.refresh(); - assert.isNotEmpty( - window.getBlockedNumbers().find(m => m.get('number') === '2') - ); - }); - - it('throws if storage is invalid', () => { - const _storage = window.storage; - window.storage = null; - assert.throws( - () => BlockedNumberController.refresh(), - 'BlockedNumberController: Could not load blocked numbers' - ); - window.storage = _storage; - }); - }); - - describe('block', () => { - beforeEach(() => { - BlockedNumberController.refresh(); - assert.isEmpty(storage.getBlockedNumbers()); - assert.isEmpty(window.getBlockedNumbers().models); - }); - - it('adds number to the blocked list', () => { - BlockedNumberController.block('1'); - - const numbers = window.getBlockedNumbers().models; - assert.lengthOf(numbers, 1); - assert.strictEqual('1', numbers[0].get('number')); - assert.deepEqual(['1'], storage.getBlockedNumbers()); - }); - - it('only blocks the same number once', () => { - BlockedNumberController.block('2'); - BlockedNumberController.block('2'); - assert.lengthOf(window.getBlockedNumbers().models, 1); - assert.deepEqual(['2'], storage.getBlockedNumbers()); - }); - - it('does not block our own number', () => { - BlockedNumberController.block(textsecure.storage.user.getNumber()); - assert.isEmpty(window.getBlockedNumbers().models); - assert.isEmpty(storage.getBlockedNumbers()); - }); - }); - - describe('unblock', () => { - beforeEach(() => { - BlockedNumberController.refresh(); - assert.isEmpty(storage.getBlockedNumbers()); - assert.isEmpty(window.getBlockedNumbers().models); - }); - - it('removes number from the blocked list', () => { - BlockedNumberController.block('1'); - BlockedNumberController.block('2'); - - assert.lengthOf(window.getBlockedNumbers().models, 2); - assert.lengthOf(storage.getBlockedNumbers(), 2); - - BlockedNumberController.unblock('1'); - - const numbers = window.getBlockedNumbers().models; - assert.lengthOf(numbers, 1); - assert.isEmpty(numbers.filter(n => n.get('number') === '1')); - assert.deepEqual(['2'], storage.getBlockedNumbers()); - }); - - it('removes number from the blocked list even if it is not present in the collection', () => { - BlockedNumberController.block('1'); - BlockedNumberController.block('2'); - window.getBlockedNumbers().reset([]); - - assert.isEmpty(window.getBlockedNumbers().models); - assert.lengthOf(storage.getBlockedNumbers(), 2); - - BlockedNumberController.unblock('1'); - assert.deepEqual(['2'], storage.getBlockedNumbers()); - }); - }); - - describe('unblockAll', () => { - it('removes all our blocked numbers', () => { - BlockedNumberController.refresh(); - - BlockedNumberController.block('1'); - BlockedNumberController.block('2'); - BlockedNumberController.block('3'); - - assert.lengthOf(window.getBlockedNumbers().models, 3); - assert.lengthOf(storage.getBlockedNumbers(), 3); - - BlockedNumberController.unblockAll(); - - assert.lengthOf(window.getBlockedNumbers().models, 0); - assert.lengthOf(storage.getBlockedNumbers(), 0); - }); - }); - - describe('isBlocked', () => { - it('returns whether a number is blocked', () => { - BlockedNumberController.refresh(); - - BlockedNumberController.block('1'); - assert.isOk(BlockedNumberController.isBlocked('1')); - assert.isNotOk(BlockedNumberController.isBlocked('2')); - - BlockedNumberController.unblock('1'); - assert.isNotOk(BlockedNumberController.isBlocked('1')); - }); - }); -}); diff --git a/test/index.html b/test/index.html index eb6a097c9..c98043693 100644 --- a/test/index.html +++ b/test/index.html @@ -505,7 +505,6 @@ - @@ -574,7 +573,6 @@ - diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index b6e73b9e4..46dd9846a 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -18,6 +18,7 @@ import { PubKey } from '../session/types'; import { handleSyncMessage } from './syncMessages'; import { onError } from './errors'; import ByteBuffer from 'bytebuffer'; +import { BlockedNumberController } from '../util/blockedNumberController'; export async function handleContentMessage(envelope: EnvelopePlus) { const plaintext = await decrypt(envelope, envelope.content); @@ -96,9 +97,7 @@ function unpad(paddedData: ArrayBuffer): ArrayBuffer { } export function isBlocked(number: string) { - // TODO: should probably use primary pubkeys here! - const blockedNumbers = window.textsecure.storage.get('blocked', []); - return blockedNumbers.indexOf(number) >= 0; + return BlockedNumberController.isBlocked(number); } async function decryptPreKeyWhisperMessage( diff --git a/ts/receiver/groups.ts b/ts/receiver/groups.ts index bac23f4f0..dedfd0ddc 100644 --- a/ts/receiver/groups.ts +++ b/ts/receiver/groups.ts @@ -3,11 +3,10 @@ import { ClosedGroupRequestInfoMessage } from '../session/messages/outgoing/cont import { getMessageQueue } from '../session'; import { PubKey } from '../session/types'; import _ from 'lodash'; +import { BlockedNumberController } from '../util/blockedNumberController'; function isGroupBlocked(groupId: string) { - return ( - window.textsecure.storage.get('blocked-groups', []).indexOf(groupId) >= 0 - ); + return BlockedNumberController.isGroupBlocked(groupId); } function shouldIgnoreBlockedGroup(group: any, senderPubKey: string) { diff --git a/ts/util/blockedNumberController.ts b/ts/util/blockedNumberController.ts new file mode 100644 index 000000000..cdfa17134 --- /dev/null +++ b/ts/util/blockedNumberController.ts @@ -0,0 +1,119 @@ +import { createOrUpdateItem, getItemById } from '../../js/modules/data'; +import { PubKey } from '../session/types'; +import { MultiDeviceProtocol } from '../session/protocols'; + +const BLOCKED_NUMBERS_ID = 'blocked'; +const BLOCKED_GROUPS_ID = 'blocked-groups'; + +// tslint:disable-next-line: no-unnecessary-class +export class BlockedNumberController { + private static loaded: boolean = false; + private static blockedNumbers: Set = new Set(); + private static blockedGroups: Set = new Set(); + + /** + * Check if a device is blocked. + * + * @param number The device. + */ + public static isBlocked(device: string | PubKey): boolean { + void this.load(); + const stringValue = + device instanceof PubKey ? device.key : device.toLowerCase(); + return this.blockedNumbers.has(stringValue); + } + + /** + * Check if a group id is blocked. + * @param groupId The group id. + */ + public static isGroupBlocked(groupId: string | PubKey): boolean { + void this.load(); + const stringValue = + groupId instanceof PubKey ? groupId.key : groupId.toLowerCase(); + return this.blockedGroups.has(stringValue); + } + + /** + * Block a user (including their linked devices). + * + * @param user The user to block. + */ + public static async block(user: string | PubKey): Promise { + // The reason we add all linked device to block number set instead of checking if any device of a user is in the `isBlocked` function because + // `isBlocked` is used synchronously in the code. To check if any device is blocked needs it to be async, which would mean all calls to `isBlocked` will also need to be async and so on + // This is too much of a hassle at the moment as some UI code will have to be migrated to work with this async call. + await this.load(); + const devices = await MultiDeviceProtocol.getAllDevices(user); + devices.forEach(pubKey => this.blockedNumbers.add(pubKey.key)); + await this.saveToDB(BLOCKED_NUMBERS_ID, this.blockedNumbers); + } + + /** + * Unblock a user (including their linked devices). + * @param user The user to unblock. + */ + public static async unblock(user: string | PubKey): Promise { + await this.load(); + const devices = await MultiDeviceProtocol.getAllDevices(user); + devices.forEach(pubKey => this.blockedNumbers.delete(pubKey.key)); + await this.saveToDB(BLOCKED_NUMBERS_ID, this.blockedNumbers); + } + + public static async blockGroup(groupId: string | PubKey): Promise { + await this.load(); + const id = PubKey.cast(groupId); + this.blockedGroups.add(id.key); + await this.saveToDB(BLOCKED_GROUPS_ID, this.blockedGroups); + } + + public static async unblockGroup(groupId: string | PubKey): Promise { + await this.load(); + const id = PubKey.cast(groupId); + this.blockedGroups.delete(id.key); + await this.saveToDB(BLOCKED_GROUPS_ID, this.blockedGroups); + } + + public static getBlockedNumbers(): Set { + return new Set(this.blockedNumbers); + } + + public static getBlockedGroups(): Set { + return new Set(this.blockedGroups); + } + + // ---- DB + + public static async load() { + if (!this.loaded) { + this.blockedNumbers = await this.getNumbersFromDB(BLOCKED_NUMBERS_ID); + this.blockedGroups = await this.getNumbersFromDB(BLOCKED_GROUPS_ID); + this.loaded = true; + } + } + + public static async reset() { + this.loaded = false; + this.blockedNumbers = new Set(); + this.blockedGroups = new Set(); + } + + private static async getNumbersFromDB(id: string): Promise> { + const data = await getItemById(id); + if (!data || !data.value) { + return new Set(); + } + + return new Set(data.value); + } + + private static async saveToDB( + id: string, + numbers: Set + ): Promise { + await createOrUpdateItem({ + id, + value: [...numbers], + }); + } +} diff --git a/ts/util/index.ts b/ts/util/index.ts index 8416f8630..c5428412b 100644 --- a/ts/util/index.ts +++ b/ts/util/index.ts @@ -6,6 +6,8 @@ import { migrateColor } from './migrateColor'; import { makeLookup } from './makeLookup'; import * as UserUtil from './user'; +export * from './blockedNumberController'; + export { arrayBufferToObjectURL, GoogleChrome,