Merge pull request #1192 from Mikunj/send-to-group

Handle typing and expiration messages when sending to group
This commit is contained in:
Mikunj Varsani 2020-06-24 15:02:33 +10:00 committed by GitHub
commit 006b5760c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 147 additions and 95 deletions

View File

@ -417,13 +417,13 @@
libsession
.getMessageQueue()
.sendUsingMultiDevice(device, typingMessage)
.ignore();
.catch(log.error);
} else {
// the recipients on the case of a group are found by the messageQueue using message.groupId
libsession
.getMessageQueue()
.sendToGroup(typingMessage)
.ignore();
.catch(log.error);
}
},
@ -1925,7 +1925,7 @@
libsession
.getMessageQueue()
.sendToGroup(groupUpdateMessage)
.ignore();
.catch(log.error);
},
sendGroupInfo(recipient) {
@ -1949,7 +1949,7 @@
libsession
.getMessageQueue()
.send(recipientPubKey, groupUpdateMessage)
.ignore();
.catch(log.error);
}
},

View File

@ -96,7 +96,7 @@ describe('Conversation', () => {
it('adds conversation to message collection upon leaving group', async () => {
const convo = new Whisper.ConversationCollection().add({
type: 'group',
id: 'a random string',
id: '052d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
});
await convo.leaveGroup();
assert.notEqual(convo.messageCollection.length, 0);

View File

@ -3,23 +3,26 @@ import { SignalService } from '../../../../protobuf';
import { TextEncoder } from 'util';
import { MessageParams } from '../Message';
import { StringUtils } from '../../../utils';
import { PubKey } from '../../../types';
interface TypingMessageParams extends MessageParams {
isTyping: boolean;
typingTimestamp?: number;
groupId?: string;
groupId?: string | PubKey;
}
export class TypingMessage extends ContentMessage {
private readonly isTyping: boolean;
private readonly typingTimestamp?: number;
private readonly groupId?: string;
public readonly isTyping: boolean;
public readonly typingTimestamp?: number;
public readonly groupId?: PubKey;
constructor(params: TypingMessageParams) {
super({ timestamp: params.timestamp, identifier: params.identifier });
this.isTyping = params.isTyping;
this.typingTimestamp = params.typingTimestamp;
this.groupId = params.groupId;
const { groupId } = params;
this.groupId = groupId ? PubKey.cast(groupId) : undefined;
}
public ttl(): number {
@ -41,7 +44,7 @@ export class TypingMessage extends ContentMessage {
const typingMessage = new SignalService.TypingMessage();
if (this.groupId) {
typingMessage.groupId = new Uint8Array(
StringUtils.encode(this.groupId, 'utf8')
StringUtils.encode(this.groupId.key, 'utf8')
);
}
typingMessage.action = action;

View File

@ -3,23 +3,26 @@ import { SignalService } from '../../../../../protobuf';
import { MessageParams } from '../../Message';
import { StringUtils } from '../../../../utils';
import { DataMessage } from './DataMessage';
import { PubKey } from '../../../../types';
interface ExpirationTimerUpdateMessageParams extends MessageParams {
groupId?: string;
groupId?: string | PubKey;
expireTimer: number | null;
profileKey?: Uint8Array;
}
export class ExpirationTimerUpdateMessage extends DataMessage {
private readonly groupId?: string;
private readonly expireTimer: number | null;
private readonly profileKey?: Uint8Array;
public readonly groupId?: PubKey;
public readonly expireTimer: number | null;
public readonly profileKey?: Uint8Array;
constructor(params: ExpirationTimerUpdateMessageParams) {
super({ timestamp: params.timestamp, identifier: params.identifier });
this.groupId = params.groupId;
this.expireTimer = params.expireTimer;
this.profileKey = params.profileKey;
const { groupId } = params;
this.groupId = groupId ? PubKey.cast(groupId) : undefined;
}
public ttl(): number {
@ -32,7 +35,7 @@ export class ExpirationTimerUpdateMessage extends DataMessage {
const groupMessage = new SignalService.GroupContext();
if (this.groupId) {
groupMessage.id = new Uint8Array(
StringUtils.encode(this.groupId, 'utf8')
StringUtils.encode(this.groupId.key, 'utf8')
);
groupMessage.type = SignalService.GroupContext.Type.DELIVER;
}

View File

@ -16,8 +16,7 @@ export abstract class ClosedGroupMessage extends DataMessage {
timestamp: params.timestamp,
identifier: params.identifier,
});
const { groupId } = params;
this.groupId = typeof groupId === 'string' ? new PubKey(groupId) : groupId;
this.groupId = PubKey.cast(params.groupId);
}
public ttl(): number {

View File

@ -3,3 +3,4 @@ export * from './DeviceUnlinkMessage';
export * from './GroupInvitationMessage';
export * from './ChatMessage';
export * from './group';
export * from './ExpirationTimerUpdateMessage';

View File

@ -140,7 +140,7 @@ export class MultiDeviceProtocol {
public static async getPairingAuthorisations(
device: PubKey | string
): Promise<Array<PairingAuthorisation>> {
const pubKey = typeof device === 'string' ? new PubKey(device) : device;
const pubKey = PubKey.cast(device);
await this.fetchPairingAuthorisationsIfNeeded(pubKey);
return getPairingAuthorisationsFor(pubKey.key);
@ -153,7 +153,7 @@ export class MultiDeviceProtocol {
public static async removePairingAuthorisations(
device: PubKey | string
): Promise<void> {
const pubKey = typeof device === 'string' ? new PubKey(device) : device;
const pubKey = PubKey.cast(device);
return removePairingAuthorisationsFor(pubKey.key);
}
@ -166,7 +166,7 @@ export class MultiDeviceProtocol {
public static async getAllDevices(
user: PubKey | string
): Promise<Array<PubKey>> {
const pubKey = typeof user === 'string' ? new PubKey(user) : user;
const pubKey = PubKey.cast(user);
const authorisations = await this.getPairingAuthorisations(pubKey);
if (authorisations.length === 0) {
return [pubKey];
@ -190,7 +190,7 @@ export class MultiDeviceProtocol {
public static async getPrimaryDevice(
user: PubKey | string
): Promise<PrimaryPubKey> {
const pubKey = typeof user === 'string' ? new PubKey(user) : user;
const pubKey = PubKey.cast(user);
const authorisations = await this.getPairingAuthorisations(pubKey);
if (authorisations.length === 0) {
return pubKey;
@ -237,7 +237,7 @@ export class MultiDeviceProtocol {
* @param device The device to check.
*/
public static async isOurDevice(device: PubKey | string): Promise<boolean> {
const pubKey = typeof device === 'string' ? new PubKey(device) : device;
const pubKey = PubKey.cast(device);
try {
const ourDevices = await this.getOurDevices();

View File

@ -6,9 +6,11 @@ import {
import {
ClosedGroupMessage,
ContentMessage,
ExpirationTimerUpdateMessage,
OpenGroupMessage,
SessionRequestMessage,
SyncMessage,
TypingMessage,
} from '../messages/outgoing';
import { PendingMessageCache } from './PendingMessageCache';
import {
@ -33,13 +35,16 @@ export class MessageQueue implements MessageQueueInterface {
void this.processAllPending();
}
public async sendUsingMultiDevice(user: PubKey, message: ContentMessage) {
public async sendUsingMultiDevice(
user: PubKey,
message: ContentMessage
): Promise<void> {
const userDevices = await MultiDeviceProtocol.getAllDevices(user.key);
await this.sendMessageToDevices(userDevices, message);
}
public async send(device: PubKey, message: ContentMessage) {
public async send(device: PubKey, message: ContentMessage): Promise<void> {
await this.sendMessageToDevices([device], message);
}
@ -75,26 +80,8 @@ export class MessageQueue implements MessageQueueInterface {
}
public async sendToGroup(
message: OpenGroupMessage | ClosedGroupMessage
): Promise<boolean> {
// Closed groups
if (message instanceof ClosedGroupMessage) {
// Get devices in closed group
const recipients = await GroupUtils.getGroupMembers(message.groupId);
if (recipients.length === 0) {
return false;
}
// Send to all devices of members
await Promise.all(
recipients.map(async recipient =>
this.sendUsingMultiDevice(recipient, message)
)
);
return true;
}
message: OpenGroupMessage | ContentMessage
): Promise<void> {
// Open groups
if (message instanceof OpenGroupMessage) {
// No queue needed for Open Groups; send directly
@ -108,20 +95,42 @@ export class MessageQueue implements MessageQueueInterface {
} else {
this.events.emit('fail', message, error);
}
return result;
} catch (e) {
console.warn(
`Failed to send message to open group: ${message.group.server}`,
e
);
this.events.emit('fail', message, error);
return false;
}
return;
}
return false;
let groupId: PubKey | undefined;
if (message instanceof ClosedGroupMessage) {
groupId = message.groupId;
} else if (message instanceof TypingMessage) {
groupId = message.groupId;
} else if (message instanceof ExpirationTimerUpdateMessage) {
groupId = message.groupId;
}
if (!groupId) {
throw new Error('Invalid group message passed in sendToGroup.');
}
// Get devices in group
const recipients = await GroupUtils.getGroupMembers(groupId);
if (recipients.length === 0) {
return;
}
// Send to all devices of members
await Promise.all(
recipients.map(async recipient =>
this.sendUsingMultiDevice(recipient, message)
)
);
}
public async sendSyncMessage(message: SyncMessage | undefined): Promise<any> {

View File

@ -17,8 +17,8 @@ export interface MessageQueueInterfaceEvents {
export interface MessageQueueInterface {
events: TypedEventEmitter<MessageQueueInterfaceEvents>;
sendUsingMultiDevice(user: PubKey, message: ContentMessage): void;
send(device: PubKey, message: ContentMessage): void;
sendToGroup(message: GroupMessageType): void;
sendUsingMultiDevice(user: PubKey, message: ContentMessage): Promise<void>;
send(device: PubKey, message: ContentMessage): Promise<void>;
sendToGroup(message: GroupMessageType): Promise<void>;
sendSyncMessage(message: SyncMessage | undefined): Promise<any>;
}

View File

@ -5,11 +5,35 @@ export class PubKey {
);
public readonly key: string;
/**
* A PubKey object.
* If `pubKeyString` is not valid then this will throw an `Error`.
*
* @param pubkeyString The public key string.
*/
constructor(pubkeyString: string) {
PubKey.validate(pubkeyString);
if (!PubKey.validate(pubkeyString)) {
throw new Error(`Invalid pubkey string passed: ${pubkeyString}`);
}
this.key = pubkeyString;
}
/**
* Cast a `value` to a `PubKey`.
* If `value` is not valid then this will throw.
*
* @param value The value to cast.
*/
public static cast(value: string | PubKey): PubKey {
return typeof value === 'string' ? new PubKey(value) : value;
}
/**
* Try convert `pubKeyString` to `PubKey`.
*
* @param pubkeyString The public key string.
* @returns `PubKey` if valid otherwise returns `undefined`.
*/
public static from(pubkeyString: string): PubKey | undefined {
// Returns a new instance if the pubkey is valid
if (PubKey.validate(pubkeyString)) {

View File

@ -6,22 +6,32 @@ import {
} from '../../../session/messages/outgoing';
import { SignalService } from '../../../protobuf';
import { TextEncoder } from 'util';
import { TestUtils } from '../../test-utils';
import { StringUtils } from '../../../session/utils';
import { PubKey } from '../../../session/types';
describe('ClosedGroupChatMessage', () => {
let groupId: PubKey;
beforeEach(() => {
groupId = TestUtils.generateFakePubKey();
});
it('can create empty message with timestamp, groupId and chatMessage', () => {
const chatMessage = new ChatMessage({
timestamp: Date.now(),
body: 'body',
});
const message = new ClosedGroupChatMessage({
groupId: '12',
groupId,
chatMessage,
});
const plainText = message.plainTextBuffer();
const decoded = SignalService.Content.decode(plainText);
expect(decoded.dataMessage)
.to.have.property('group')
.to.have.deep.property('id', new TextEncoder().encode('12'));
.to.have.deep.property(
'id',
new Uint8Array(StringUtils.encode(groupId.key, 'utf8'))
);
expect(decoded.dataMessage)
.to.have.property('group')
.to.have.deep.property('type', SignalService.GroupContext.Type.DELIVER);
@ -39,7 +49,7 @@ describe('ClosedGroupChatMessage', () => {
timestamp: Date.now(),
});
const message = new ClosedGroupChatMessage({
groupId: '12',
groupId,
chatMessage,
});
expect(message.ttl()).to.equal(24 * 60 * 60 * 1000);
@ -50,7 +60,7 @@ describe('ClosedGroupChatMessage', () => {
timestamp: Date.now(),
});
const message = new ClosedGroupChatMessage({
groupId: '12',
groupId,
chatMessage,
});
expect(message.identifier).to.not.equal(null, 'identifier cannot be null');
@ -67,7 +77,7 @@ describe('ClosedGroupChatMessage', () => {
identifier: 'chatMessage',
});
const message = new ClosedGroupChatMessage({
groupId: '12',
groupId,
chatMessage,
identifier: 'closedGroupMessage',
});
@ -81,7 +91,7 @@ describe('ClosedGroupChatMessage', () => {
identifier: 'chatMessage',
});
const message = new ClosedGroupChatMessage({
groupId: '12',
groupId,
chatMessage,
});
expect(message.identifier).to.be.equal('chatMessage');

View File

@ -5,6 +5,8 @@ import { SignalService } from '../../../protobuf';
import { TextEncoder } from 'util';
import Long from 'long';
import { toNumber } from 'lodash';
import { StringUtils } from '../../../session/utils';
import { TestUtils } from '../../test-utils';
describe('TypingMessage', () => {
it('has Action.STARTED if isTyping = true', () => {
@ -60,7 +62,7 @@ describe('TypingMessage', () => {
});
it('has groupId set if a value given', () => {
const groupId = '6666666666';
const groupId = TestUtils.generateFakePubKey();
const message = new TypingMessage({
timestamp: Date.now(),
isTyping: true,
@ -68,7 +70,9 @@ describe('TypingMessage', () => {
});
const plainText = message.plainTextBuffer();
const decoded = SignalService.Content.decode(plainText);
const manuallyEncodedGroupId = new TextEncoder().encode(groupId);
const manuallyEncodedGroupId = new Uint8Array(
StringUtils.encode(groupId.key, 'utf8')
);
expect(decoded.typingMessage?.groupId).to.be.deep.equal(
manuallyEncodedGroupId

View File

@ -12,7 +12,7 @@ import { PubKey } from '../../../session/types';
describe('SessionProtocol', () => {
const sandbox = sinon.createSandbox();
const ourNumber = 'ourNumber';
const pubkey = new PubKey('deviceid');
const pubkey = TestUtils.generateFakePubKey();
let getItemById: sinon.SinonStub;
let send: sinon.SinonStub;
@ -88,14 +88,14 @@ describe('SessionProtocol', () => {
it('protocol: sendSessionRequest should add the deviceID to the sentMap', async () => {
expect(SessionProtocol.getSentSessionsTimestamp())
.to.have.property('deviceid')
.to.have.property(pubkey.key)
.to.be.approximately(Date.now(), 100);
});
it('protocol: sendSessionRequest should not have pendingSend set after', async () => {
expect(
SessionProtocol.getPendingSendSessionTimestamp()
).to.not.have.property('deviceid');
).to.not.have.property(pubkey.key);
});
});
@ -107,34 +107,32 @@ describe('SessionProtocol', () => {
it('protocol: onSessionEstablished should remove the device in sentTimestamps', async () => {
expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property(
'deviceid'
pubkey.key
);
await SessionProtocol.onSessionEstablished(pubkey);
expect(SessionProtocol.getSentSessionsTimestamp()).to.not.have.property(
'deviceid'
pubkey.key
);
});
it('protocol: onSessionEstablished should remove the device in sentTimestamps and ONLY that one', async () => {
// add a second item to the map
await SessionProtocol.sendSessionRequest(
resetMessage,
new PubKey('deviceid2')
);
const anotherPubKey = TestUtils.generateFakePubKey();
await SessionProtocol.sendSessionRequest(resetMessage, anotherPubKey);
expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property(
'deviceid'
pubkey.key
);
expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property(
'deviceid2'
anotherPubKey.key
);
await SessionProtocol.onSessionEstablished(pubkey);
expect(SessionProtocol.getSentSessionsTimestamp()).to.not.have.property(
'deviceid'
pubkey.key
);
expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property(
'deviceid2'
anotherPubKey.key
);
});
});
@ -144,7 +142,7 @@ describe('SessionProtocol', () => {
const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey);
expect(hasSent).to.be.equal(
false,
'hasSent should be false for `deviceid`'
`hasSent should be false for ${pubkey.key}`
);
});
@ -154,7 +152,7 @@ describe('SessionProtocol', () => {
const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey);
expect(hasSent).to.be.equal(
true,
'hasSent should be true for `deviceid`'
`hasSent should be true for ${pubkey.key}`
);
});
@ -174,7 +172,7 @@ describe('SessionProtocol', () => {
const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey);
expect(hasSent).to.be.equal(
true,
'hasSent should be true for `deviceid`'
`hasSent should be true for ${pubkey.key}`
);
});
@ -189,7 +187,7 @@ describe('SessionProtocol', () => {
const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey);
expect(hasSent).to.be.equal(
true,
'hasSent should be true for `deviceid`'
`hasSent should be true for ${pubkey.key}`
);
send.resetHistory();
@ -207,7 +205,7 @@ describe('SessionProtocol', () => {
// trigger the requestProcessed and check the map is updated
await SessionProtocol.onSessionRequestProcessed(pubkey);
expect(SessionProtocol.getProcessedSessionsTimestamp())
.to.have.property('deviceid')
.to.have.property(pubkey.key)
.to.be.approximately(Date.now(), 5);
});
@ -217,14 +215,15 @@ describe('SessionProtocol', () => {
await SessionProtocol.onSessionRequestProcessed(pubkey);
expect(SessionProtocol.getProcessedSessionsTimestamp())
.to.have.property('deviceid')
.to.have.property(pubkey.key)
.to.be.approximately(Date.now(), 5);
await TestUtils.timeout(5);
const oldTimestamp = SessionProtocol.getProcessedSessionsTimestamp()
.deviceid;
const oldTimestamp = SessionProtocol.getProcessedSessionsTimestamp()[
pubkey.key
];
await SessionProtocol.onSessionRequestProcessed(pubkey);
expect(SessionProtocol.getProcessedSessionsTimestamp())
.to.have.property('deviceid')
.to.have.property(pubkey.key)
.to.be.approximately(Date.now(), 5)
.to.not.be.equal(oldTimestamp);
});

View File

@ -312,6 +312,13 @@ describe('MessageQueue', () => {
});
describe('sendToGroup', () => {
it('should throw an error if invalid non-group message was passed', async () => {
const chatMessage = TestUtils.generateChatMessage();
await expect(
messageQueueStub.sendToGroup(chatMessage)
).to.be.rejectedWith('Invalid group message passed in sendToGroup.');
});
describe('closed groups', async () => {
it('can send to closed group', async () => {
const members = TestUtils.generateFakePubKeys(4).map(
@ -324,8 +331,7 @@ describe('MessageQueue', () => {
.resolves();
const message = TestUtils.generateClosedGroupMessage();
const success = await messageQueueStub.sendToGroup(message);
expect(success).to.equal(true, 'sending to group failed');
await messageQueueStub.sendToGroup(message);
expect(sendUsingMultiDeviceStub.callCount).to.equal(members.length);
const arg = sendUsingMultiDeviceStub.getCall(0).args;
@ -342,12 +348,7 @@ describe('MessageQueue', () => {
.resolves();
const message = TestUtils.generateClosedGroupMessage();
const response = await messageQueueStub.sendToGroup(message);
expect(response).to.equal(
false,
'sendToGroup sent a message to an empty group'
);
await messageQueueStub.sendToGroup(message);
expect(sendUsingMultiDeviceStub.callCount).to.equal(0);
});
});
@ -365,9 +366,8 @@ describe('MessageQueue', () => {
it('can send to open group', async () => {
const message = TestUtils.generateOpenGroupMessage();
const success = await messageQueueStub.sendToGroup(message);
await messageQueueStub.sendToGroup(message);
expect(sendToOpenGroupStub.callCount).to.equal(1);
expect(success).to.equal(true, 'Sending to open group failed');
});
it('should emit a success event when send was successful', async () => {