diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index b07b94851..fca857e8f 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -1006,6 +1006,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 f342aeab9..cf695b3d3 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 e4ad6d87c..86c228e57 100644
--- a/js/conversation_controller.js
+++ b/js/conversation_controller.js
@@ -196,7 +196,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 06f490fdb..6e7174669 100644
--- a/js/models/conversations.js
+++ b/js/models/conversations.js
@@ -223,20 +223,48 @@
return !!(this.id && this.id.match(/^rss:/));
},
isBlocked() {
- return BlockedNumberController.isBlocked(this.id);
+ if (!this.id || this.isMe()) {
+ return false;
+ }
+
+ if (this.isClosedGroup()) {
+ return BlockedNumberController.isGroupBlocked(this.id);
+ }
+
+ if (this.isPrivate()) {
+ const primary = this.getPrimaryDevicePubKey();
+ return BlockedNumberController.isBlocked(primary);
+ }
+
+ return false;
},
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;
@@ -772,6 +800,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..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
@@ -535,6 +536,9 @@
case 'left-group':
placeholder = i18n('sendMessageLeftGroup');
break;
+ case 'blocked-user':
+ placeholder = i18n('sendMessageBlockedUser');
+ break;
default:
placeholder = i18n('sendMessage');
break;
@@ -1907,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/preload.js b/preload.js
index c9f918a8e..2337f2171 100644
--- a/preload.js
+++ b/preload.js
@@ -195,7 +195,7 @@ window.resetDatabase = () => {
ipc.send('resetDatabase');
};
-window.onUnblockNumber = number => {
+window.onUnblockNumber = async number => {
// Unblock the number
if (window.BlockedNumberController) {
window.BlockedNumberController.unblock(number);
@@ -205,7 +205,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: ',
@@ -495,3 +495,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 b8b6e310a..3e8d105b9 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);
@@ -95,10 +96,8 @@ function unpad(paddedData: ArrayBuffer): ArrayBuffer {
throw new Error('Invalid padding');
}
-export function isBlocked(number: string) {
- // TODO: should probably use primary pubkeys here!
- const blockedNumbers = window.textsecure.storage.get('blocked', []);
- return blockedNumbers.indexOf(number) >= 0;
+export async function isBlocked(number: string) {
+ return BlockedNumberController.isBlockedAsync(number);
}
async function decryptPreKeyWhisperMessage(
@@ -152,7 +151,9 @@ 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) {
window.log.info(
'Dropping blocked message with error after sealed sender decryption'
);
@@ -187,7 +188,8 @@ async function decryptUnidentifiedSender(
envelope.type = SignalService.Envelope.Type.SESSION_REQUEST;
}
- if (isBlocked(sender.getName())) {
+ const blocked = await isBlocked(sender.getName());
+ if (blocked) {
window.log.info('Dropping blocked message after sealed sender decryption');
return null;
}
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/receiver/receiver.ts b/ts/receiver/receiver.ts
index 3b50cdb93..a4f31f11e 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 {
@@ -152,7 +153,8 @@ 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) {
return;
}
diff --git a/ts/test/util/blockedNumberController_test.ts b/ts/test/util/blockedNumberController_test.ts
new file mode 100644
index 000000000..02c1c7ce9
--- /dev/null
+++ b/ts/test/util/blockedNumberController_test.ts
@@ -0,0 +1,240 @@
+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';
+import { UserUtil } from '../../util';
+
+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 the primary device of the user', async () => {
+ const primary = TestUtils.generateFakePubKey();
+ const secondary = TestUtils.generateFakePubKey();
+ sandbox.stub(MultiDeviceProtocol, 'getPrimaryDevice').resolves(primary);
+
+ await BlockedNumberController.block(secondary);
+
+ const blockedNumbers = BlockedNumberController.getBlockedNumbers();
+ 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 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(secondary);
+
+ 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 () => {
+ const pubKey = TestUtils.generateFakePubKey();
+ const another = TestUtils.generateFakePubKey();
+ memoryDB.blocked = [pubKey.key, another.key];
+ sandbox.stub(MultiDeviceProtocol, 'getPrimaryDevice').resolves(pubKey);
+
+ await BlockedNumberController.unblock(pubKey);
+
+ 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);
+ });
+ });
+
+ 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(memoryDB['blocked-groups']).to.have.lengthOf(1);
+ expect(memoryDB['blocked-groups']).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);
+ expect(memoryDB['blocked-groups']).to.have.lengthOf(1);
+ expect(memoryDB['blocked-groups']).to.include(another.key);
+ });
+ });
+
+ describe('isBlocked', async () => {
+ it('should return true if number is blocked', 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'
+ );
+ });
+
+ 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 true if group is blocked', 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'
+ );
+ });
+
+ 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
new file mode 100644
index 000000000..baaabe7d4
--- /dev/null
+++ b/ts/util/blockedNumberController.ts
@@ -0,0 +1,148 @@
+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.
+ * 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.
+ */
+ public static isBlocked(device: 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 =
+ device instanceof PubKey ? device.key : device.toLowerCase();
+ return this.blockedNumbers.has(stringValue);
+ }
+
+ /**
+ * 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 {
+ const stringValue =
+ groupId instanceof PubKey ? groupId.key : groupId.toLowerCase();
+ return this.blockedGroups.has(stringValue);
+ }
+
+ /**
+ * Block a user.
+ * This will only block the primary device of the user.
+ *
+ * @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 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.
+ * 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 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 {
+ 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(): Array {
+ return [...this.blockedNumbers];
+ }
+
+ public static getBlockedGroups(): Array {
+ return [...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 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,