Merge pull request #1222 from Mikunj/blocking

Fix up Blocking
pull/1224/head
Mikunj Varsani 5 years ago committed by GitHub
commit f11e0f2cd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1006,6 +1006,9 @@
"sendMessageLeftGroup": {
"message": "You left this group"
},
"sendMessageBlockedUser": {
"message": "You have blocked the user"
},
"groupMembers": {
"message": "Group members"
},

@ -463,7 +463,6 @@
<script type='text/javascript' src='js/registration.js'></script>
<script type='text/javascript' src='js/expire.js'></script>
<script type='text/javascript' src='js/conversation_controller.js'></script>
<script type='text/javascript' src='js/blocked_number_controller.js'></script>
<script type='text/javascript' src='js/message_controller.js'></script>
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>

@ -463,7 +463,6 @@
<script type='text/javascript' src='js/registration.js'></script>
<script type='text/javascript' src='js/expire.js'></script>
<script type='text/javascript' src='js/conversation_controller.js'></script>
<script type='text/javascript' src='js/blocked_number_controller.js'></script>
<script type='text/javascript' src='js/message_controller.js'></script>
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>

@ -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:',

@ -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);
},
};
})();

@ -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) {

@ -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');

@ -898,7 +898,7 @@
ConversationController.reset();
BlockedNumberController.reset();
await ConversationController.load();
BlockedNumberController.refresh();
await BlockedNumberController.load();
},
async removeAllConfiguration() {
await window.Signal.Data.removeAllConfiguration();

@ -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(
`<option value="${number}">${this.truncate(number, 25)}</option>`
);
}
},
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;
},
});
})();

@ -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';
}

@ -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;

@ -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'));
});
});
});

@ -505,7 +505,6 @@
<script type="text/javascript" src="../js/models/conversations.js" data-cover></script>
<script type="text/javascript" src="../js/models/blockedNumbers.js" data-cover></script>
<script type="text/javascript" src="../js/conversation_controller.js" data-cover></script>
<script type="text/javascript" src="../js/blocked_number_controller.js"></script>
<script type="text/javascript" src="../js/message_controller.js" data-cover></script>
<script type="text/javascript" src="../js/keychange_listener.js" data-cover></script>
<script type="text/javascript" src="../js/expiring_messages.js" data-cover></script>
@ -574,7 +573,6 @@
<script type="text/javascript" src="models/conversations_test.js"></script>
<script type="text/javascript" src="models/messages_test.js"></script>
<script type="text/javascript" src="blocked_number_controller_test.js"></script>
<script type="text/javascript" src="conversation_controller_test.js"></script>
<script type="text/javascript" src="storage_test.js"></script>
<script type="text/javascript" src="keychange_listener_test.js"></script>

@ -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;
}

@ -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) {

@ -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;
}

@ -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<PubKey>;
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'
);
});
});
});

@ -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<string> = new Set();
private static blockedGroups: Set<string> = 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<boolean> {
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<void> {
// 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<void> {
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<void> {
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<void> {
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<string> {
return [...this.blockedNumbers];
}
public static getBlockedGroups(): Array<string> {
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<Set<string>> {
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<string>
): Promise<void> {
await createOrUpdateItem({
id,
value: [...numbers],
});
}
}

@ -6,6 +6,8 @@ import { migrateColor } from './migrateColor';
import { makeLookup } from './makeLookup';
import * as UserUtil from './user';
export * from './blockedNumberController';
export {
arrayBufferToObjectURL,
GoogleChrome,

Loading…
Cancel
Save