Merge remote-tracking branch 'upstream/clearnet' into move-models-to-ts-2
commit
36fab86d30
@ -0,0 +1,124 @@
|
||||
// this is not a very good name, but a configuration message is a message sent to our other devices so sync our current public and closed groups
|
||||
|
||||
import { ContentMessage } from './ContentMessage';
|
||||
import { SignalService } from '../../../../protobuf';
|
||||
import { MessageParams } from '../Message';
|
||||
import { Constants } from '../../..';
|
||||
import { ECKeyPair } from '../../../../receiver/keypairs';
|
||||
import { fromHexToArray } from '../../../utils/String';
|
||||
import { PubKey } from '../../../types';
|
||||
|
||||
interface ConfigurationMessageParams extends MessageParams {
|
||||
activeClosedGroups: Array<ConfigurationMessageClosedGroup>;
|
||||
activeOpenGroups: Array<string>;
|
||||
}
|
||||
|
||||
export class ConfigurationMessage extends ContentMessage {
|
||||
public readonly activeClosedGroups: Array<ConfigurationMessageClosedGroup>;
|
||||
public readonly activeOpenGroups: Array<string>;
|
||||
|
||||
constructor(params: ConfigurationMessageParams) {
|
||||
super({ timestamp: params.timestamp, identifier: params.identifier });
|
||||
this.activeClosedGroups = params.activeClosedGroups;
|
||||
this.activeOpenGroups = params.activeOpenGroups;
|
||||
|
||||
if (!this.activeClosedGroups) {
|
||||
throw new Error('closed group must be set');
|
||||
}
|
||||
|
||||
if (!this.activeOpenGroups) {
|
||||
throw new Error('open group must be set');
|
||||
}
|
||||
}
|
||||
|
||||
public ttl(): number {
|
||||
return Constants.TTL_DEFAULT.TYPING_MESSAGE;
|
||||
}
|
||||
|
||||
public contentProto(): SignalService.Content {
|
||||
return new SignalService.Content({
|
||||
configurationMessage: this.configurationProto(),
|
||||
});
|
||||
}
|
||||
|
||||
protected configurationProto(): SignalService.ConfigurationMessage {
|
||||
return new SignalService.ConfigurationMessage({
|
||||
closedGroups: this.mapClosedGroupsObjectToProto(this.activeClosedGroups),
|
||||
openGroups: this.activeOpenGroups,
|
||||
});
|
||||
}
|
||||
|
||||
private mapClosedGroupsObjectToProto(
|
||||
closedGroups: Array<ConfigurationMessageClosedGroup>
|
||||
): Array<SignalService.ConfigurationMessage.ClosedGroup> {
|
||||
return (closedGroups || []).map(m =>
|
||||
new ConfigurationMessageClosedGroup(m).toProto()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigurationMessageClosedGroup {
|
||||
public publicKey: string;
|
||||
public name: string;
|
||||
public encryptionKeyPair: ECKeyPair;
|
||||
public members: Array<string>;
|
||||
public admins: Array<string>;
|
||||
|
||||
public constructor({
|
||||
publicKey,
|
||||
name,
|
||||
encryptionKeyPair,
|
||||
members,
|
||||
admins,
|
||||
}: {
|
||||
publicKey: string;
|
||||
name: string;
|
||||
encryptionKeyPair: ECKeyPair;
|
||||
members: Array<string>;
|
||||
admins: Array<string>;
|
||||
}) {
|
||||
this.publicKey = publicKey;
|
||||
this.name = name;
|
||||
this.encryptionKeyPair = encryptionKeyPair;
|
||||
this.members = members;
|
||||
this.admins = admins;
|
||||
|
||||
// will throw if publik key is invalid
|
||||
PubKey.cast(publicKey);
|
||||
|
||||
if (
|
||||
!encryptionKeyPair?.privateKeyData?.byteLength ||
|
||||
!encryptionKeyPair?.publicKeyData?.byteLength
|
||||
) {
|
||||
throw new Error('Encryption key pair looks invalid');
|
||||
}
|
||||
|
||||
if (!this.name?.length) {
|
||||
throw new Error('name must be set');
|
||||
}
|
||||
|
||||
if (!this.members?.length) {
|
||||
throw new Error('members must be set');
|
||||
}
|
||||
if (!this.admins?.length) {
|
||||
throw new Error('admins must be set');
|
||||
}
|
||||
|
||||
if (this.admins.some(a => !this.members.includes(a))) {
|
||||
throw new Error('some admins are not members');
|
||||
}
|
||||
}
|
||||
|
||||
public toProto(): SignalService.ConfigurationMessage.ClosedGroup {
|
||||
return new SignalService.ConfigurationMessage.ClosedGroup({
|
||||
publicKey: fromHexToArray(this.publicKey),
|
||||
name: this.name,
|
||||
encryptionKeyPair: {
|
||||
publicKey: this.encryptionKeyPair.publicKeyData,
|
||||
privateKey: this.encryptionKeyPair.privateKeyData,
|
||||
},
|
||||
members: this.members.map(fromHexToArray),
|
||||
admins: this.admins.map(fromHexToArray),
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import { createOrUpdateItem, getItemById } from '../../../js/modules/data';
|
||||
import { getMessageQueue } from '..';
|
||||
import { ConversationController } from '../conversations';
|
||||
import { getCurrentConfigurationMessage } from './Messages';
|
||||
import { RawMessage } from '../types';
|
||||
import { DAYS } from './Number';
|
||||
|
||||
const ITEM_ID_LAST_SYNC_TIMESTAMP = 'lastSyncedTimestamp';
|
||||
|
||||
const getLastSyncTimestampFromDb = async (): Promise<number | undefined> =>
|
||||
(await getItemById(ITEM_ID_LAST_SYNC_TIMESTAMP))?.value;
|
||||
|
||||
const writeLastSyncTimestampToDb = async (timestamp: number) =>
|
||||
createOrUpdateItem({ id: ITEM_ID_LAST_SYNC_TIMESTAMP, value: timestamp });
|
||||
|
||||
export const syncConfigurationIfNeeded = async () => {
|
||||
const lastSyncedTimestamp = (await getLastSyncTimestampFromDb()) || 0;
|
||||
const now = Date.now();
|
||||
|
||||
// if the last sync was less than 2 days before, return early.
|
||||
if (Math.abs(now - lastSyncedTimestamp) < DAYS * 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allConvos = ConversationController.getInstance().getConversations();
|
||||
const configMessage = await getCurrentConfigurationMessage(allConvos);
|
||||
try {
|
||||
window.log.info('syncConfigurationIfNeeded with', configMessage);
|
||||
|
||||
await getMessageQueue().sendSyncMessage(configMessage);
|
||||
} catch (e) {
|
||||
window.log.warn(
|
||||
'Caught an error while sending our ConfigurationMessage:',
|
||||
e
|
||||
);
|
||||
// we do return early so that next time we use the old timestamp again
|
||||
// and so try again to trigger a sync
|
||||
return;
|
||||
}
|
||||
await writeLastSyncTimestampToDb(now);
|
||||
};
|
||||
|
||||
export const forceSyncConfigurationNowIfNeeded = async (
|
||||
waitForMessageSent = false
|
||||
) => {
|
||||
const allConvos = ConversationController.getInstance().getConversations();
|
||||
const configMessage = await getCurrentConfigurationMessage(allConvos);
|
||||
window.log.info('forceSyncConfigurationNowIfNeeded with', configMessage);
|
||||
|
||||
const waitForMessageSentEvent = new Promise(resolve => {
|
||||
const ourResolver = (message: any) => {
|
||||
if (message.identifier === configMessage.identifier) {
|
||||
getMessageQueue().events.off('sendSuccess', ourResolver);
|
||||
getMessageQueue().events.off('sendFail', ourResolver);
|
||||
resolve(true);
|
||||
}
|
||||
};
|
||||
getMessageQueue().events.on('sendSuccess', ourResolver);
|
||||
getMessageQueue().events.on('sendFail', ourResolver);
|
||||
});
|
||||
|
||||
try {
|
||||
// this just adds the message to the sending queue.
|
||||
// if waitForMessageSent is set, we need to effectively wait until then
|
||||
await Promise.all([
|
||||
getMessageQueue().sendSyncMessage(configMessage),
|
||||
waitForMessageSentEvent,
|
||||
]);
|
||||
} catch (e) {
|
||||
window.log.warn(
|
||||
'Caught an error while sending our ConfigurationMessage:',
|
||||
e
|
||||
);
|
||||
}
|
||||
if (!waitForMessageSent) {
|
||||
return;
|
||||
}
|
||||
|
||||
return waitForMessageSentEvent;
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { getPasswordHash } from '../../js/modules/data';
|
||||
|
||||
export async function hasPassword() {
|
||||
// @ts-ignore
|
||||
const hash = await window.Signal.Data.getPasswordHash();
|
||||
const hash = await getPasswordHash();
|
||||
|
||||
return !!hash;
|
||||
}
|
||||
|
@ -0,0 +1,141 @@
|
||||
import { expect } from 'chai';
|
||||
import { ECKeyPair } from '../../../../receiver/keypairs';
|
||||
|
||||
import {
|
||||
ConfigurationMessage,
|
||||
ConfigurationMessageClosedGroup,
|
||||
} from '../../../../session/messages/outgoing/content/ConfigurationMessage';
|
||||
import { PubKey } from '../../../../session/types';
|
||||
import { TestUtils } from '../../../test-utils';
|
||||
|
||||
describe('ConfigurationMessage', () => {
|
||||
it('throw if closed group is not set', () => {
|
||||
const activeClosedGroups = null as any;
|
||||
const params = {
|
||||
activeClosedGroups,
|
||||
activeOpenGroups: [],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
expect(() => new ConfigurationMessage(params)).to.throw(
|
||||
'closed group must be set'
|
||||
);
|
||||
});
|
||||
|
||||
it('throw if open group is not set', () => {
|
||||
const activeOpenGroups = null as any;
|
||||
const params = {
|
||||
activeClosedGroups: [],
|
||||
activeOpenGroups,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
expect(() => new ConfigurationMessage(params)).to.throw(
|
||||
'open group must be set'
|
||||
);
|
||||
});
|
||||
|
||||
describe('ConfigurationMessageClosedGroup', () => {
|
||||
it('throw if closed group has no encryptionkeypair', () => {
|
||||
const member = TestUtils.generateFakePubKey().key;
|
||||
const params = {
|
||||
publicKey: TestUtils.generateFakePubKey().key,
|
||||
name: 'groupname',
|
||||
members: [member],
|
||||
admins: [member],
|
||||
encryptionKeyPair: undefined as any,
|
||||
};
|
||||
|
||||
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(
|
||||
'Encryption key pair looks invalid'
|
||||
);
|
||||
});
|
||||
|
||||
it('throw if closed group has invalid encryptionkeypair', () => {
|
||||
const member = TestUtils.generateFakePubKey().key;
|
||||
const params = {
|
||||
publicKey: TestUtils.generateFakePubKey().key,
|
||||
name: 'groupname',
|
||||
members: [member],
|
||||
admins: [member],
|
||||
encryptionKeyPair: new ECKeyPair(new Uint8Array(), new Uint8Array()),
|
||||
};
|
||||
|
||||
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(
|
||||
'Encryption key pair looks invalid'
|
||||
);
|
||||
});
|
||||
|
||||
it('throw if closed group has invalid pubkey', () => {
|
||||
const member = TestUtils.generateFakePubKey().key;
|
||||
const params = {
|
||||
publicKey: 'invalidpubkey',
|
||||
name: 'groupname',
|
||||
members: [member],
|
||||
admins: [member],
|
||||
encryptionKeyPair: TestUtils.generateFakeECKeyPair(),
|
||||
};
|
||||
|
||||
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw();
|
||||
});
|
||||
|
||||
it('throw if closed group has invalid name', () => {
|
||||
const member = TestUtils.generateFakePubKey().key;
|
||||
const params = {
|
||||
publicKey: TestUtils.generateFakePubKey().key,
|
||||
name: '',
|
||||
members: [member],
|
||||
admins: [member],
|
||||
encryptionKeyPair: TestUtils.generateFakeECKeyPair(),
|
||||
};
|
||||
|
||||
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(
|
||||
'name must be set'
|
||||
);
|
||||
});
|
||||
|
||||
it('throw if members is empty', () => {
|
||||
const member = TestUtils.generateFakePubKey().key;
|
||||
const params = {
|
||||
publicKey: TestUtils.generateFakePubKey().key,
|
||||
name: 'groupname',
|
||||
members: [],
|
||||
admins: [member],
|
||||
encryptionKeyPair: TestUtils.generateFakeECKeyPair(),
|
||||
};
|
||||
|
||||
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(
|
||||
'members must be set'
|
||||
);
|
||||
});
|
||||
|
||||
it('throw if admins is empty', () => {
|
||||
const member = TestUtils.generateFakePubKey().key;
|
||||
const params = {
|
||||
publicKey: TestUtils.generateFakePubKey().key,
|
||||
name: 'groupname',
|
||||
members: [member],
|
||||
admins: [],
|
||||
encryptionKeyPair: TestUtils.generateFakeECKeyPair(),
|
||||
};
|
||||
|
||||
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(
|
||||
'admins must be set'
|
||||
);
|
||||
});
|
||||
|
||||
it('throw if some admins are not members', () => {
|
||||
const member = TestUtils.generateFakePubKey().key;
|
||||
const admin = TestUtils.generateFakePubKey().key;
|
||||
const params = {
|
||||
publicKey: TestUtils.generateFakePubKey().key,
|
||||
name: 'groupname',
|
||||
members: [member],
|
||||
admins: [admin],
|
||||
encryptionKeyPair: TestUtils.generateFakeECKeyPair(),
|
||||
};
|
||||
|
||||
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(
|
||||
'some admins are not members'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,31 @@
|
||||
import chai from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { ConversationController } from '../../../../session/conversations';
|
||||
import * as MessageUtils from '../../../../session/utils/Messages';
|
||||
import { syncConfigurationIfNeeded } from '../../../../session/utils/syncUtils';
|
||||
import { TestUtils } from '../../../test-utils';
|
||||
import { restoreStubs } from '../../../test-utils/utils';
|
||||
// tslint:disable-next-line: no-require-imports no-var-requires
|
||||
const chaiAsPromised = require('chai-as-promised');
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
describe('SyncUtils', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
restoreStubs();
|
||||
});
|
||||
|
||||
describe('syncConfigurationIfNeeded', () => {
|
||||
it('sync if last sync undefined', async () => {
|
||||
// TestUtils.stubData('getItemById').resolves(undefined);
|
||||
// sandbox.stub(ConversationController.getInstance(), 'getConversations').returns([]);
|
||||
// const getCurrentConfigurationMessageSpy = sandbox.spy(MessageUtils, 'getCurrentConfigurationMessage');
|
||||
// await syncConfigurationIfNeeded();
|
||||
// expect(getCurrentConfigurationMessageSpy.callCount).equal(1);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue