From aa75205bbd9d4a4af60308be38efb4b38bbc719d Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 7 Jul 2020 09:13:55 +1000 Subject: [PATCH 1/7] 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, From 8c3943094d6a9013b99eee21c3fe0f3171c83ec5 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 7 Jul 2020 11:42:56 +1000 Subject: [PATCH 2/7] Add tests --- js/views/conversation_view.js | 11 +- ts/test/util/blockedNumberController_test.ts | 176 +++++++++++++++++++ ts/util/blockedNumberController.ts | 10 +- 3 files changed, 190 insertions(+), 7 deletions(-) create mode 100644 ts/test/util/blockedNumberController_test.ts diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 21eff707c..d139cbed6 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -10,6 +10,7 @@ textsecure, Whisper, ConversationController, + BlockedNumberController, */ // eslint-disable-next-line func-names @@ -1910,11 +1911,17 @@ toastOptions.id = 'clockOutOfSync'; } } - if (this.model.isPrivate() && storage.isBlocked(this.model.id)) { + if ( + this.model.isPrivate() && + BlockedNumberController.isBlocked(this.model.id) + ) { toastOptions.title = i18n('unblockToSend'); toastOptions.id = 'unblockToSend'; } - if (!this.model.isPrivate() && storage.isGroupBlocked(this.model.id)) { + if ( + !this.model.isPrivate() && + BlockedNumberController.isGroupBlocked(this.model.id) + ) { toastOptions.title = i18n('unblockGroupToSend'); toastOptions.id = 'unblockGroupToSend'; } diff --git a/ts/test/util/blockedNumberController_test.ts b/ts/test/util/blockedNumberController_test.ts new file mode 100644 index 000000000..79d952d4d --- /dev/null +++ b/ts/test/util/blockedNumberController_test.ts @@ -0,0 +1,176 @@ +import { expect } from 'chai'; +import * as crypto from 'crypto'; +import Sinon, * as sinon from 'sinon'; +import { BlockedNumberController } from '../../util/blockedNumberController'; +import { TestUtils } from '../test-utils'; +import { PubKey } from '../../session/types'; +import { MultiDeviceProtocol } from '../../session/protocols'; + +describe('BlockedNumberController', () => { + const sandbox = sinon.createSandbox(); + let memoryDB: { [key: string]: any }; + beforeEach(() => { + memoryDB = {}; + + TestUtils.stubData('createOrUpdateItem').callsFake(data => { + memoryDB[data.id] = data.value; + }); + + TestUtils.stubData('getItemById').callsFake(id => { + if (!memoryDB[id]) { + return undefined; + } + const value = memoryDB[id]; + return { + id, + value, + }; + }); + + BlockedNumberController.reset(); + }); + + afterEach(() => { + sandbox.restore(); + TestUtils.restoreStubs(); + }); + + describe('load', async () => { + it('should load data from the database', async () => { + const normal = TestUtils.generateFakePubKey(); + const group = TestUtils.generateFakePubKey(); + memoryDB.blocked = [normal.key]; + memoryDB['blocked-groups'] = [group.key]; + await BlockedNumberController.load(); + + const blockedNumbers = BlockedNumberController.getBlockedNumbers(); + const blockedGroups = BlockedNumberController.getBlockedGroups(); + + expect(blockedNumbers).to.have.lengthOf(1); + expect(blockedNumbers).to.include(normal.key); + expect(blockedGroups).to.have.lengthOf(1); + expect(blockedGroups).to.include(group.key); + }); + + it('should return empty if nothing in the db exists', async () => { + await BlockedNumberController.load(); + const blockedNumbers = BlockedNumberController.getBlockedNumbers(); + const blockedGroups = BlockedNumberController.getBlockedGroups(); + + expect(blockedNumbers).to.be.empty; + expect(blockedGroups).to.be.empty; + }); + }); + + describe('block', async () => { + it('should block all linked devices of a user', async () => { + const pubKey = TestUtils.generateFakePubKey(); + const linkedDevice = TestUtils.generateFakePubKey(); + sandbox + .stub(MultiDeviceProtocol, 'getAllDevices') + .resolves([pubKey, linkedDevice]); + + await BlockedNumberController.block(linkedDevice); + + const blockedNumbers = BlockedNumberController.getBlockedNumbers(); + expect(blockedNumbers).to.have.lengthOf(2); + expect(blockedNumbers).to.have.same.members([ + pubKey.key, + linkedDevice.key, + ]); + expect(BlockedNumberController.getBlockedGroups()).to.be.empty; + }); + }); + + describe('unblock', async () => { + it('should unblock all linked device of a user', async () => { + const pubKey = TestUtils.generateFakePubKey(); + const linkedDevice = TestUtils.generateFakePubKey(); + memoryDB.blocked = [pubKey.key, linkedDevice.key]; + sandbox + .stub(MultiDeviceProtocol, 'getAllDevices') + .resolves([pubKey, linkedDevice]); + + await BlockedNumberController.unblock(linkedDevice); + + const blockedNumbers = BlockedNumberController.getBlockedNumbers(); + expect(blockedNumbers).to.be.empty; + }); + + it('should only unblock if a device was blocked', async () => { + const pubKey = TestUtils.generateFakePubKey(); + const another = TestUtils.generateFakePubKey(); + memoryDB.blocked = [pubKey.key, another.key]; + sandbox.stub(MultiDeviceProtocol, 'getAllDevices').resolves([pubKey]); + + await BlockedNumberController.unblock(pubKey); + + const blockedNumbers = BlockedNumberController.getBlockedNumbers(); + expect(blockedNumbers).to.have.lengthOf(1); + expect(blockedNumbers).to.include(another.key); + }); + }); + + describe('blockGroup', async () => { + it('should block a group', async () => { + const group = TestUtils.generateFakePubKey(); + + await BlockedNumberController.blockGroup(group); + + const blockedGroups = BlockedNumberController.getBlockedGroups(); + expect(blockedGroups).to.have.lengthOf(1); + expect(blockedGroups).to.include(group.key); + expect(BlockedNumberController.getBlockedNumbers()).to.be.empty; + }); + }); + + describe('unblockGroup', async () => { + it('should unblock a group', async () => { + const group = TestUtils.generateFakePubKey(); + const another = TestUtils.generateFakePubKey(); + memoryDB['blocked-groups'] = [group.key, another.key]; + + await BlockedNumberController.unblockGroup(group); + + const blockedGroups = BlockedNumberController.getBlockedGroups(); + expect(blockedGroups).to.have.lengthOf(1); + expect(blockedGroups).to.include(another.key); + }); + }); + + describe('isBlocked', async () => { + it('should return the correct value', async () => { + const pubKey = TestUtils.generateFakePubKey(); + const groupPubKey = TestUtils.generateFakePubKey(); + memoryDB.blocked = [pubKey.key]; + memoryDB['blocked-groups'] = [groupPubKey.key]; + await BlockedNumberController.load(); + expect(BlockedNumberController.isBlocked(pubKey.key)).to.equal( + true, + 'Expected isBlocked to return true for user pubkey' + ); + expect(BlockedNumberController.isBlocked(groupPubKey.key)).to.equal( + false, + 'Expected isBlocked to return false for a group pubkey' + ); + }); + }); + + describe('isGroupBlocked', async () => { + it('should return the correct value', async () => { + const pubKey = TestUtils.generateFakePubKey(); + const groupPubKey = TestUtils.generateFakePubKey(); + memoryDB.blocked = [pubKey.key]; + memoryDB['blocked-groups'] = [groupPubKey.key]; + await BlockedNumberController.load(); + expect(BlockedNumberController.isGroupBlocked(pubKey.key)).to.equal( + false, + 'Expected isGroupBlocked to return false for user pubkey' + ); + expect(BlockedNumberController.isGroupBlocked(groupPubKey.key)).to.equal( + true, + 'Expected isGroupBlocked to return true for a group pubkey' + ); + }); + }); +}); diff --git a/ts/util/blockedNumberController.ts b/ts/util/blockedNumberController.ts index cdfa17134..74edd88e2 100644 --- a/ts/util/blockedNumberController.ts +++ b/ts/util/blockedNumberController.ts @@ -74,12 +74,12 @@ export class BlockedNumberController { await this.saveToDB(BLOCKED_GROUPS_ID, this.blockedGroups); } - public static getBlockedNumbers(): Set { - return new Set(this.blockedNumbers); + public static getBlockedNumbers(): Array { + return [...this.blockedNumbers]; } - public static getBlockedGroups(): Set { - return new Set(this.blockedGroups); + public static getBlockedGroups(): Array { + return [...this.blockedGroups]; } // ---- DB @@ -92,7 +92,7 @@ export class BlockedNumberController { } } - public static async reset() { + public static reset() { this.loaded = false; this.blockedNumbers = new Set(); this.blockedGroups = new Set(); From 95f1e34b6b6e3de1af9e9e3b5442c873db755ef5 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 7 Jul 2020 11:49:18 +1000 Subject: [PATCH 3/7] Update tests --- ts/test/util/blockedNumberController_test.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ts/test/util/blockedNumberController_test.ts b/ts/test/util/blockedNumberController_test.ts index 79d952d4d..bfc133280 100644 --- a/ts/test/util/blockedNumberController_test.ts +++ b/ts/test/util/blockedNumberController_test.ts @@ -72,12 +72,11 @@ describe('BlockedNumberController', () => { await BlockedNumberController.block(linkedDevice); + const expected = [pubKey.key, linkedDevice.key]; const blockedNumbers = BlockedNumberController.getBlockedNumbers(); expect(blockedNumbers).to.have.lengthOf(2); - expect(blockedNumbers).to.have.same.members([ - pubKey.key, - linkedDevice.key, - ]); + expect(blockedNumbers).to.have.same.members(expected); + expect(memoryDB.blocked).to.have.same.members(expected); expect(BlockedNumberController.getBlockedGroups()).to.be.empty; }); }); @@ -95,6 +94,7 @@ describe('BlockedNumberController', () => { const blockedNumbers = BlockedNumberController.getBlockedNumbers(); expect(blockedNumbers).to.be.empty; + expect(memoryDB.blocked).to.be.empty; }); it('should only unblock if a device was blocked', async () => { @@ -108,6 +108,8 @@ describe('BlockedNumberController', () => { const blockedNumbers = BlockedNumberController.getBlockedNumbers(); expect(blockedNumbers).to.have.lengthOf(1); expect(blockedNumbers).to.include(another.key); + expect(memoryDB.blocked).to.have.lengthOf(1); + expect(memoryDB.blocked).to.include(another.key); }); }); @@ -120,6 +122,8 @@ describe('BlockedNumberController', () => { const blockedGroups = BlockedNumberController.getBlockedGroups(); expect(blockedGroups).to.have.lengthOf(1); expect(blockedGroups).to.include(group.key); + expect(memoryDB['blocked-groups']).to.have.lengthOf(1); + expect(memoryDB['blocked-groups']).to.include(group.key); expect(BlockedNumberController.getBlockedNumbers()).to.be.empty; }); }); @@ -135,6 +139,8 @@ describe('BlockedNumberController', () => { const blockedGroups = BlockedNumberController.getBlockedGroups(); expect(blockedGroups).to.have.lengthOf(1); expect(blockedGroups).to.include(another.key); + expect(memoryDB['blocked-groups']).to.have.lengthOf(1); + expect(memoryDB['blocked-groups']).to.include(another.key); }); }); From 1b93a210bc91608103718db84e1520c687c0f309 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 7 Jul 2020 13:46:56 +1000 Subject: [PATCH 4/7] Comments --- ts/util/blockedNumberController.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ts/util/blockedNumberController.ts b/ts/util/blockedNumberController.ts index 74edd88e2..050cbd6a6 100644 --- a/ts/util/blockedNumberController.ts +++ b/ts/util/blockedNumberController.ts @@ -13,11 +13,13 @@ export class BlockedNumberController { /** * Check if a device is blocked. + * Make sure `load()` has been called before this function so that the correct blocked state is returned. * * @param number The device. */ public static isBlocked(device: string | PubKey): boolean { - void this.load(); + // This function is not `async` because the old `isBlocked` function in js was also not async. + // To convert it means we'll have to re-wire all our UI components to work with async. const stringValue = device instanceof PubKey ? device.key : device.toLowerCase(); return this.blockedNumbers.has(stringValue); @@ -25,10 +27,13 @@ export class BlockedNumberController { /** * Check if a group id is blocked. + * Make sure `load()` has been called before this function so that the correct blocked state is returned. + * * @param groupId The group id. */ public static isGroupBlocked(groupId: string | PubKey): boolean { - void this.load(); + // This function is not `async` because the old `isBlocked` function in js was also not async. + // To convert it means we'll have to re-wire all our UI components to work with async. const stringValue = groupId instanceof PubKey ? groupId.key : groupId.toLowerCase(); return this.blockedGroups.has(stringValue); From 3159baebfafbce294ac8bd07ea332eb4bddccfd7 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 7 Jul 2020 14:41:37 +1000 Subject: [PATCH 5/7] Fix case where blocked user adds a new linked device. --- js/models/conversations.js | 18 ++++++++++++++---- ts/receiver/contentMessage.ts | 17 +++++++++++++---- ts/receiver/receiver.ts | 5 ++++- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index b98d92c02..150787057 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -224,13 +224,23 @@ return !!(this.id && this.id.match(/^rss:/)); }, isBlocked() { - if (!this.id || this.isPublic() || this.isRss()) { + if (this.isMe()) { return false; } - return this.isPrivate() - ? BlockedNumberController.isBlocked(this.id) - : BlockedNumberController.isGroupBlocked(this.id); + if (this.isClosedGroup()) { + return BlockedNumberController.isGroupBlocked(this.id); + } + + if (this.isPrivate()) { + const primary = this.getPrimaryDevicePubKey(); + return ( + BlockedNumberController.isBlocked(primary) || + BlockedNumberController.isBlocked(this.id) + ); + } + + return false; }, isMediumGroup() { return this.get('is_medium_group'); diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 46dd9846a..a27dca199 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -96,8 +96,12 @@ function unpad(paddedData: ArrayBuffer): ArrayBuffer { throw new Error('Invalid padding'); } -export function isBlocked(number: string) { - return BlockedNumberController.isBlocked(number); +export async function isBlocked(number: string) { + const primary = await MultiDeviceProtocol.getPrimaryDevice(number); + return ( + BlockedNumberController.isBlocked(primary) || + BlockedNumberController.isBlocked(number) + ); } async function decryptPreKeyWhisperMessage( @@ -151,7 +155,10 @@ async function decryptUnidentifiedSender( const { sender: source } = error || {}; if (source) { - if (isBlocked(source.getName())) { + // tslint:disable-next-line: no-shadowed-variable + const blocked = await isBlocked(source.getName()); + if (blocked) { + await BlockedNumberController.block(source.getName()); window.log.info( 'Dropping blocked message with error after sealed sender decryption' ); @@ -186,7 +193,9 @@ async function decryptUnidentifiedSender( envelope.type = SignalService.Envelope.Type.SESSION_REQUEST; } - if (isBlocked(sender.getName())) { + const blocked = await isBlocked(sender.getName()); + if (blocked) { + await BlockedNumberController.block(sender.getName()); window.log.info('Dropping blocked message after sealed sender decryption'); return null; } diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 0fdf69fd4..daeb711fb 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -30,6 +30,7 @@ import { import { getEnvelopeId } from './common'; import { StringUtils } from '../session/utils'; import { SignalService } from '../protobuf'; +import { BlockedNumberController } from '../util/blockedNumberController'; // TODO: check if some of these exports no longer needed export { @@ -153,7 +154,9 @@ async function handleRequestDetail( // TODO: 'source' is almost certainly undefined here (sealed sender), // so this check is not appropriate here - if (isBlocked(envelope.source)) { + const blocked = await isBlocked(envelope.source); + if (blocked) { + await BlockedNumberController.block(envelope.source); return; } From 2a1afa9fe72ed06ffa1e4f473996cb6102d9e13e Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 7 Jul 2020 14:46:01 +1000 Subject: [PATCH 6/7] Forgot checking for our own devices --- js/models/conversations.js | 2 +- ts/receiver/contentMessage.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 150787057..1f098d500 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -224,7 +224,7 @@ return !!(this.id && this.id.match(/^rss:/)); }, isBlocked() { - if (this.isMe()) { + if (!this.id || this.isMe()) { return false; } diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index a27dca199..2ef992b2b 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -97,6 +97,11 @@ function unpad(paddedData: ArrayBuffer): ArrayBuffer { } export async function isBlocked(number: string) { + const ourDevice = await MultiDeviceProtocol.isOurDevice(number); + if (ourDevice) { + return false; + } + const primary = await MultiDeviceProtocol.getPrimaryDevice(number); return ( BlockedNumberController.isBlocked(primary) || From f008a8b3efc9bd2008a7402d857499ce23896e82 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 7 Jul 2020 15:28:25 +1000 Subject: [PATCH 7/7] Always block the primary device instead of all devices. Added isBlockedAsync function. --- js/models/conversations.js | 5 +- ts/receiver/contentMessage.ts | 13 +-- ts/receiver/receiver.ts | 1 - ts/test/util/blockedNumberController_test.ts | 102 +++++++++++++++---- ts/util/blockedNumberController.ts | 44 ++++++-- 5 files changed, 116 insertions(+), 49 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 1f098d500..7b4b277b3 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -234,10 +234,7 @@ if (this.isPrivate()) { const primary = this.getPrimaryDevicePubKey(); - return ( - BlockedNumberController.isBlocked(primary) || - BlockedNumberController.isBlocked(this.id) - ); + return BlockedNumberController.isBlocked(primary); } return false; diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 2ef992b2b..5b108f313 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -97,16 +97,7 @@ function unpad(paddedData: ArrayBuffer): ArrayBuffer { } export async function isBlocked(number: string) { - const ourDevice = await MultiDeviceProtocol.isOurDevice(number); - if (ourDevice) { - return false; - } - - const primary = await MultiDeviceProtocol.getPrimaryDevice(number); - return ( - BlockedNumberController.isBlocked(primary) || - BlockedNumberController.isBlocked(number) - ); + return BlockedNumberController.isBlockedAsync(number); } async function decryptPreKeyWhisperMessage( @@ -163,7 +154,6 @@ async function decryptUnidentifiedSender( // tslint:disable-next-line: no-shadowed-variable const blocked = await isBlocked(source.getName()); if (blocked) { - await BlockedNumberController.block(source.getName()); window.log.info( 'Dropping blocked message with error after sealed sender decryption' ); @@ -200,7 +190,6 @@ async function decryptUnidentifiedSender( const blocked = await isBlocked(sender.getName()); if (blocked) { - await BlockedNumberController.block(sender.getName()); window.log.info('Dropping blocked message after sealed sender decryption'); return null; } diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index daeb711fb..5dcedb9a2 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -156,7 +156,6 @@ async function handleRequestDetail( // so this check is not appropriate here const blocked = await isBlocked(envelope.source); if (blocked) { - await BlockedNumberController.block(envelope.source); return; } diff --git a/ts/test/util/blockedNumberController_test.ts b/ts/test/util/blockedNumberController_test.ts index bfc133280..02c1c7ce9 100644 --- a/ts/test/util/blockedNumberController_test.ts +++ b/ts/test/util/blockedNumberController_test.ts @@ -5,6 +5,7 @@ import { BlockedNumberController } from '../../util/blockedNumberController'; import { TestUtils } from '../test-utils'; import { PubKey } from '../../session/types'; import { MultiDeviceProtocol } from '../../session/protocols'; +import { UserUtil } from '../../util'; describe('BlockedNumberController', () => { const sandbox = sinon.createSandbox(); @@ -63,34 +64,29 @@ describe('BlockedNumberController', () => { }); describe('block', async () => { - it('should block all linked devices of a user', async () => { - const pubKey = TestUtils.generateFakePubKey(); - const linkedDevice = TestUtils.generateFakePubKey(); - sandbox - .stub(MultiDeviceProtocol, 'getAllDevices') - .resolves([pubKey, linkedDevice]); + it('should block the primary device of the user', async () => { + const primary = TestUtils.generateFakePubKey(); + const secondary = TestUtils.generateFakePubKey(); + sandbox.stub(MultiDeviceProtocol, 'getPrimaryDevice').resolves(primary); - await BlockedNumberController.block(linkedDevice); + await BlockedNumberController.block(secondary); - const expected = [pubKey.key, linkedDevice.key]; const blockedNumbers = BlockedNumberController.getBlockedNumbers(); - expect(blockedNumbers).to.have.lengthOf(2); - expect(blockedNumbers).to.have.same.members(expected); - expect(memoryDB.blocked).to.have.same.members(expected); + expect(blockedNumbers).to.have.lengthOf(1); + expect(blockedNumbers).to.include(primary.key); + expect(memoryDB.blocked).to.include(primary.key); expect(BlockedNumberController.getBlockedGroups()).to.be.empty; }); }); describe('unblock', async () => { - it('should unblock all linked device of a user', async () => { - const pubKey = TestUtils.generateFakePubKey(); - const linkedDevice = TestUtils.generateFakePubKey(); - memoryDB.blocked = [pubKey.key, linkedDevice.key]; - sandbox - .stub(MultiDeviceProtocol, 'getAllDevices') - .resolves([pubKey, linkedDevice]); + it('should unblock the primary device', async () => { + const primary = TestUtils.generateFakePubKey(); + const secondary = TestUtils.generateFakePubKey(); + memoryDB.blocked = [primary.key]; + sandbox.stub(MultiDeviceProtocol, 'getPrimaryDevice').resolves(primary); - await BlockedNumberController.unblock(linkedDevice); + await BlockedNumberController.unblock(secondary); const blockedNumbers = BlockedNumberController.getBlockedNumbers(); expect(blockedNumbers).to.be.empty; @@ -101,7 +97,7 @@ describe('BlockedNumberController', () => { const pubKey = TestUtils.generateFakePubKey(); const another = TestUtils.generateFakePubKey(); memoryDB.blocked = [pubKey.key, another.key]; - sandbox.stub(MultiDeviceProtocol, 'getAllDevices').resolves([pubKey]); + sandbox.stub(MultiDeviceProtocol, 'getPrimaryDevice').resolves(pubKey); await BlockedNumberController.unblock(pubKey); @@ -145,7 +141,7 @@ describe('BlockedNumberController', () => { }); describe('isBlocked', async () => { - it('should return the correct value', async () => { + it('should return true if number is blocked', async () => { const pubKey = TestUtils.generateFakePubKey(); const groupPubKey = TestUtils.generateFakePubKey(); memoryDB.blocked = [pubKey.key]; @@ -160,10 +156,62 @@ describe('BlockedNumberController', () => { 'Expected isBlocked to return false for a group pubkey' ); }); + + it('should return false if number is not blocked', async () => { + const pubKey = TestUtils.generateFakePubKey(); + memoryDB.blocked = []; + await BlockedNumberController.load(); + expect(BlockedNumberController.isBlocked(pubKey.key)).to.equal( + false, + 'Expected isBlocked to return false' + ); + }); + }); + + describe('isBlockedAsync', () => { + let ourDevices: Array; + beforeEach(() => { + ourDevices = TestUtils.generateFakePubKeys(2); + sandbox.stub(MultiDeviceProtocol, 'getOurDevices').resolves(ourDevices); + }); + it('should return false for our device', async () => { + for (const device of ourDevices) { + const isBlocked = await BlockedNumberController.isBlockedAsync(device); + expect(isBlocked).to.equal( + false, + 'Expected our devices to return false' + ); + } + }); + + it('should return true if the primary device is blocked', async () => { + const primary = TestUtils.generateFakePubKey(); + const secondary = TestUtils.generateFakePubKey(); + sandbox.stub(MultiDeviceProtocol, 'getPrimaryDevice').resolves(primary); + memoryDB.blocked = [primary.key]; + + const isBlocked = await BlockedNumberController.isBlockedAsync(secondary); + expect(isBlocked).to.equal( + true, + 'Expected isBlockedAsync to return true.' + ); + }); + + it('should return false if device is not blocked', async () => { + const primary = TestUtils.generateFakePubKey(); + sandbox.stub(MultiDeviceProtocol, 'getPrimaryDevice').resolves(primary); + memoryDB.blocked = []; + + const isBlocked = await BlockedNumberController.isBlockedAsync(primary); + expect(isBlocked).to.equal( + false, + 'Expected isBlockedAsync to return false.' + ); + }); }); describe('isGroupBlocked', async () => { - it('should return the correct value', async () => { + it('should return true if group is blocked', async () => { const pubKey = TestUtils.generateFakePubKey(); const groupPubKey = TestUtils.generateFakePubKey(); memoryDB.blocked = [pubKey.key]; @@ -178,5 +226,15 @@ describe('BlockedNumberController', () => { 'Expected isGroupBlocked to return true for a group pubkey' ); }); + + it('should return false if group is not blocked', async () => { + const groupPubKey = TestUtils.generateFakePubKey(); + memoryDB['blocked-groups'] = []; + await BlockedNumberController.load(); + expect(BlockedNumberController.isGroupBlocked(groupPubKey.key)).to.equal( + false, + 'Expected isGroupBlocked to return false' + ); + }); }); }); diff --git a/ts/util/blockedNumberController.ts b/ts/util/blockedNumberController.ts index 050cbd6a6..baaabe7d4 100644 --- a/ts/util/blockedNumberController.ts +++ b/ts/util/blockedNumberController.ts @@ -13,6 +13,25 @@ export class BlockedNumberController { /** * Check if a device is blocked. + * This will use `MultiDeviceProtocol` to determine wether a user is blocked or not. + * + * @param user The user. + */ + public static async isBlockedAsync(user: string | PubKey): Promise { + await this.load(); + const isOurDevice = await MultiDeviceProtocol.isOurDevice(user); + if (isOurDevice) { + return false; + } + + const primary = await MultiDeviceProtocol.getPrimaryDevice(user); + return this.blockedNumbers.has(primary.key); + } + + /** + * Check if a device is blocked synchronously. + * This will only check against the memory cache on if a device is blocked, it is reccomended to pass in the primary device pub key. + * * Make sure `load()` has been called before this function so that the correct blocked state is returned. * * @param number The device. @@ -32,15 +51,14 @@ export class BlockedNumberController { * @param groupId The group id. */ public static isGroupBlocked(groupId: string | PubKey): boolean { - // This function is not `async` because the old `isBlocked` function in js was also not async. - // To convert it means we'll have to re-wire all our UI components to work with async. const stringValue = groupId instanceof PubKey ? groupId.key : groupId.toLowerCase(); return this.blockedGroups.has(stringValue); } /** - * Block a user (including their linked devices). + * Block a user. + * This will only block the primary device of the user. * * @param user The user to block. */ @@ -49,20 +67,26 @@ export class BlockedNumberController { // `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); + const primary = await MultiDeviceProtocol.getPrimaryDevice(user); + if (!this.blockedNumbers.has(primary.key)) { + this.blockedNumbers.add(primary.key); + await this.saveToDB(BLOCKED_NUMBERS_ID, this.blockedNumbers); + } } /** - * Unblock a user (including their linked devices). + * Unblock a user. + * This will only unblock the primary device of the user. + * * @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); + const primary = await MultiDeviceProtocol.getPrimaryDevice(user); + if (this.blockedNumbers.has(primary.key)) { + this.blockedNumbers.delete(primary.key); + await this.saveToDB(BLOCKED_NUMBERS_ID, this.blockedNumbers); + } } public static async blockGroup(groupId: string | PubKey): Promise {