Improve reaction targeting
This commit is contained in:
parent
16f9b64435
commit
cc24f0524b
4 changed files with 571 additions and 11 deletions
|
@ -14,7 +14,13 @@ import { ReactionSource } from '../reactions/ReactionSource';
|
||||||
import { DataReader, DataWriter } from '../sql/Client';
|
import { DataReader, DataWriter } from '../sql/Client';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { getAuthor, isIncoming, isOutgoing } from '../messages/helpers';
|
import {
|
||||||
|
getAuthor,
|
||||||
|
isIncoming,
|
||||||
|
isIncomingStory,
|
||||||
|
isOutgoing,
|
||||||
|
isOutgoingStory,
|
||||||
|
} from '../messages/helpers';
|
||||||
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
|
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
|
||||||
import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
|
import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
|
||||||
import {
|
import {
|
||||||
|
@ -67,6 +73,7 @@ function remove(reaction: ReactionAttributesType): void {
|
||||||
export function findReactionsForMessage(
|
export function findReactionsForMessage(
|
||||||
message: ReadonlyMessageAttributesType
|
message: ReadonlyMessageAttributesType
|
||||||
): Array<ReactionAttributesType> {
|
): Array<ReactionAttributesType> {
|
||||||
|
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||||
const matchingReactions = Array.from(reactionCache.values()).filter(
|
const matchingReactions = Array.from(reactionCache.values()).filter(
|
||||||
reaction => {
|
reaction => {
|
||||||
return isMessageAMatchForReaction({
|
return isMessageAMatchForReaction({
|
||||||
|
@ -74,6 +81,7 @@ export function findReactionsForMessage(
|
||||||
targetTimestamp: reaction.targetTimestamp,
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
targetAuthorAci: reaction.targetAuthorAci,
|
targetAuthorAci: reaction.targetAuthorAci,
|
||||||
reactionSenderConversationId: reaction.fromId,
|
reactionSenderConversationId: reaction.fromId,
|
||||||
|
ourAci,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -94,6 +102,7 @@ async function findMessageForReaction({
|
||||||
logId: string;
|
logId: string;
|
||||||
}): Promise<MessageAttributesType | undefined> {
|
}): Promise<MessageAttributesType | undefined> {
|
||||||
const messages = await DataReader.getMessagesBySentAt(targetTimestamp);
|
const messages = await DataReader.getMessagesBySentAt(targetTimestamp);
|
||||||
|
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||||
|
|
||||||
const matchingMessages = messages.filter(message =>
|
const matchingMessages = messages.filter(message =>
|
||||||
isMessageAMatchForReaction({
|
isMessageAMatchForReaction({
|
||||||
|
@ -101,6 +110,7 @@ async function findMessageForReaction({
|
||||||
targetTimestamp,
|
targetTimestamp,
|
||||||
targetAuthorAci,
|
targetAuthorAci,
|
||||||
reactionSenderConversationId,
|
reactionSenderConversationId,
|
||||||
|
ourAci,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -119,16 +129,18 @@ async function findMessageForReaction({
|
||||||
return matchingMessages[0];
|
return matchingMessages[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMessageAMatchForReaction({
|
export function isMessageAMatchForReaction({
|
||||||
message,
|
message,
|
||||||
targetTimestamp,
|
targetTimestamp,
|
||||||
targetAuthorAci,
|
targetAuthorAci,
|
||||||
reactionSenderConversationId,
|
reactionSenderConversationId,
|
||||||
|
ourAci,
|
||||||
}: {
|
}: {
|
||||||
message: ReadonlyMessageAttributesType;
|
message: ReadonlyMessageAttributesType;
|
||||||
targetTimestamp: number;
|
targetTimestamp: number;
|
||||||
targetAuthorAci: string;
|
targetAuthorAci: string;
|
||||||
reactionSenderConversationId: string;
|
reactionSenderConversationId: string;
|
||||||
|
ourAci: AciString;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
if (!getMessageSentTimestampSet(message).has(targetTimestamp)) {
|
if (!getMessageSentTimestampSet(message).has(targetTimestamp)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -158,7 +170,7 @@ function isMessageAMatchForReaction({
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === 'outgoing') {
|
if (isOutgoing(message) || isOutgoingStory(message, ourAci)) {
|
||||||
const sendStateByConversationId = getPropForTimestamp({
|
const sendStateByConversationId = getPropForTimestamp({
|
||||||
log,
|
log,
|
||||||
message,
|
message,
|
||||||
|
@ -172,13 +184,20 @@ function isMessageAMatchForReaction({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isStory(message)) {
|
||||||
|
return (
|
||||||
|
isSent(sendState.status) && Boolean(sendState.isAllowedToReplyToStory)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return isSent(sendState.status);
|
return isSent(sendState.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === 'incoming') {
|
if (isIncoming(message) || isIncomingStory(message, ourAci)) {
|
||||||
const messageConversation = window.ConversationController.get(
|
const messageConversation = window.ConversationController.get(
|
||||||
message.conversationId
|
message.conversationId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!messageConversation) {
|
if (!messageConversation) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -190,7 +209,8 @@ function isMessageAMatchForReaction({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// Only incoming, outgoing, and story messages can be reacted to
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function onReaction(
|
export async function onReaction(
|
||||||
|
|
|
@ -11,7 +11,7 @@ import type {
|
||||||
QuotedAttachmentType,
|
QuotedAttachmentType,
|
||||||
QuotedMessageType,
|
QuotedMessageType,
|
||||||
} from '../model-types.d';
|
} from '../model-types.d';
|
||||||
import type { ServiceIdString } from '../types/ServiceId';
|
import type { AciString, ServiceIdString } from '../types/ServiceId';
|
||||||
import { PaymentEventKind } from '../types/Payment';
|
import { PaymentEventKind } from '../types/Payment';
|
||||||
import type { AnyPaymentEvent } from '../types/Payment';
|
import type { AnyPaymentEvent } from '../types/Payment';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
@ -35,6 +35,27 @@ export function isStory(
|
||||||
return message.type === 'story';
|
return message.type === 'story';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFromUs(
|
||||||
|
message: Pick<ReadonlyMessageAttributesType, 'sourceServiceId'>,
|
||||||
|
ourAci: AciString
|
||||||
|
) {
|
||||||
|
return message.sourceServiceId === ourAci;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOutgoingStory(
|
||||||
|
message: Pick<ReadonlyMessageAttributesType, 'type' | 'sourceServiceId'>,
|
||||||
|
ourAci: AciString
|
||||||
|
): boolean {
|
||||||
|
return isStory(message) && isFromUs(message, ourAci);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isIncomingStory(
|
||||||
|
message: Pick<ReadonlyMessageAttributesType, 'type' | 'sourceServiceId'>,
|
||||||
|
ourAci: AciString
|
||||||
|
): boolean {
|
||||||
|
return isStory(message) && !isFromUs(message, ourAci);
|
||||||
|
}
|
||||||
|
|
||||||
export type MessageAttributesWithPaymentEvent = ReadonlyMessageAttributesType &
|
export type MessageAttributesWithPaymentEvent = ReadonlyMessageAttributesType &
|
||||||
ReadonlyDeep<{
|
ReadonlyDeep<{
|
||||||
payment: AnyPaymentEvent;
|
payment: AnyPaymentEvent;
|
||||||
|
|
|
@ -3348,16 +3348,14 @@ export class ConversationModel extends window.Backbone
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastMessage = this.get('timestamp') || Date.now();
|
const timestamp = Date.now();
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
'adding verified change advisory for',
|
'adding verified change advisory for',
|
||||||
this.idForLogging(),
|
this.idForLogging(),
|
||||||
verifiedChangeId,
|
verifiedChangeId,
|
||||||
lastMessage
|
timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const message = new MessageModel({
|
const message = new MessageModel({
|
||||||
...generateMessageId(incrementMessageCounter()),
|
...generateMessageId(incrementMessageCounter()),
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
|
@ -3365,7 +3363,7 @@ export class ConversationModel extends window.Backbone
|
||||||
readStatus: ReadStatus.Read,
|
readStatus: ReadStatus.Read,
|
||||||
received_at_ms: timestamp,
|
received_at_ms: timestamp,
|
||||||
seenStatus: options.local ? SeenStatus.Seen : SeenStatus.Unseen,
|
seenStatus: options.local ? SeenStatus.Seen : SeenStatus.Unseen,
|
||||||
sent_at: lastMessage,
|
sent_at: timestamp,
|
||||||
timestamp,
|
timestamp,
|
||||||
type: 'verified-change',
|
type: 'verified-change',
|
||||||
verified,
|
verified,
|
||||||
|
|
521
ts/test-electron/util/reactions_test.ts
Normal file
521
ts/test-electron/util/reactions_test.ts
Normal file
|
@ -0,0 +1,521 @@
|
||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { v4 as generateGuid } from 'uuid';
|
||||||
|
import { DataWriter } from '../../sql/Client';
|
||||||
|
import { generateAci, generatePni } from '../../types/ServiceId';
|
||||||
|
import { isMessageAMatchForReaction } from '../../messageModifiers/Reactions';
|
||||||
|
import { generateMessageId } from '../../util/generateMessageId';
|
||||||
|
import { incrementMessageCounter } from '../../util/incrementMessageCounter';
|
||||||
|
import type { ConversationModel } from '../../models/conversations';
|
||||||
|
import type { MessageAttributesType } from '../../model-types';
|
||||||
|
import { SendStatus } from '../../messages/MessageSendState';
|
||||||
|
|
||||||
|
describe('isMessageAMatchForReaction', () => {
|
||||||
|
let contactA: ConversationModel;
|
||||||
|
let contactB: ConversationModel;
|
||||||
|
let contactC: ConversationModel;
|
||||||
|
let ourConversation: ConversationModel;
|
||||||
|
const OUR_ACI = generateAci();
|
||||||
|
const OUR_PNI = generatePni();
|
||||||
|
beforeEach(async () => {
|
||||||
|
await DataWriter.removeAll();
|
||||||
|
await window.textsecure.storage.user.setCredentials({
|
||||||
|
number: '+15550000000',
|
||||||
|
aci: OUR_ACI,
|
||||||
|
pni: OUR_PNI,
|
||||||
|
deviceId: 2,
|
||||||
|
deviceName: 'my device',
|
||||||
|
password: 'password',
|
||||||
|
});
|
||||||
|
|
||||||
|
window.ConversationController.reset();
|
||||||
|
await window.ConversationController.load();
|
||||||
|
|
||||||
|
contactA = window.ConversationController.getOrCreate(
|
||||||
|
generateAci(),
|
||||||
|
'private'
|
||||||
|
);
|
||||||
|
contactB = window.ConversationController.getOrCreate(
|
||||||
|
generateAci(),
|
||||||
|
'private'
|
||||||
|
);
|
||||||
|
contactC = window.ConversationController.getOrCreate(
|
||||||
|
generateAci(),
|
||||||
|
'private'
|
||||||
|
);
|
||||||
|
|
||||||
|
ourConversation = window.ConversationController.getOrCreate(
|
||||||
|
OUR_ACI,
|
||||||
|
'private'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('incoming 1:1 message', () => {
|
||||||
|
let message: MessageAttributesType;
|
||||||
|
beforeEach(() => {
|
||||||
|
message = {
|
||||||
|
...generateMessageId(incrementMessageCounter()),
|
||||||
|
type: 'incoming',
|
||||||
|
timestamp: 123,
|
||||||
|
sent_at: 123,
|
||||||
|
conversationId: contactA.id,
|
||||||
|
sourceServiceId: contactA.attributes.serviceId,
|
||||||
|
source: contactA.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches on our reaction', async () => {
|
||||||
|
assert.isTrue(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: contactA.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: ourConversation.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does not match if target author differs', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: contactB.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: contactA.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does not match if reaction sender is not in the conversation', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: contactA.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: contactB.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not match if timestamp differs', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 124,
|
||||||
|
targetAuthorAci: contactA.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: contactA.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does match if timestamp matches edit history', async () => {
|
||||||
|
assert.isTrue(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message: {
|
||||||
|
...message,
|
||||||
|
editHistory: [
|
||||||
|
{
|
||||||
|
timestamp: 124,
|
||||||
|
received_at: 124,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
targetTimestamp: 124,
|
||||||
|
targetAuthorAci: contactA.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: contactA.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("matches on sender's own reaction", async () => {
|
||||||
|
assert.isTrue(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: contactA.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: contactA.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does not match if reaction comes from a different sender', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: OUR_ACI,
|
||||||
|
reactionSenderConversationId: contactB.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('outgoing 1:1 message', () => {
|
||||||
|
let message: MessageAttributesType;
|
||||||
|
beforeEach(() => {
|
||||||
|
message = {
|
||||||
|
...generateMessageId(incrementMessageCounter()),
|
||||||
|
type: 'outgoing',
|
||||||
|
timestamp: 123,
|
||||||
|
sent_at: 123,
|
||||||
|
conversationId: contactA.id,
|
||||||
|
sourceServiceId: ourConversation.attributes.serviceId,
|
||||||
|
source: ourConversation.id,
|
||||||
|
sendStateByConversationId: {
|
||||||
|
[contactA.id]: {
|
||||||
|
status: SendStatus.Sent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
it("matches on recipient's reaction", async () => {
|
||||||
|
assert.isTrue(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: OUR_ACI,
|
||||||
|
reactionSenderConversationId: contactA.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('matches on our own reaction', async () => {
|
||||||
|
assert.isTrue(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: OUR_ACI,
|
||||||
|
reactionSenderConversationId: ourConversation.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does not match if reaction comes from a different sender', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: OUR_ACI,
|
||||||
|
reactionSenderConversationId: contactB.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does not match if message not fully sent', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message: {
|
||||||
|
...message,
|
||||||
|
sendStateByConversationId: {
|
||||||
|
[contactA.id]: {
|
||||||
|
status: SendStatus.Pending,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: OUR_ACI,
|
||||||
|
reactionSenderConversationId: contactB.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('incoming group message', () => {
|
||||||
|
let message: MessageAttributesType;
|
||||||
|
let group: ConversationModel;
|
||||||
|
beforeEach(() => {
|
||||||
|
group = window.ConversationController.getOrCreate(
|
||||||
|
generateGuid(),
|
||||||
|
'group',
|
||||||
|
{
|
||||||
|
groupVersion: 2,
|
||||||
|
membersV2: [
|
||||||
|
{
|
||||||
|
aci: contactA.getCheckedAci(''),
|
||||||
|
joinedAtVersion: 2,
|
||||||
|
role: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
aci: contactB.getCheckedAci(''),
|
||||||
|
joinedAtVersion: 2,
|
||||||
|
role: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
aci: OUR_ACI,
|
||||||
|
joinedAtVersion: 2,
|
||||||
|
role: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
message = {
|
||||||
|
...generateMessageId(incrementMessageCounter()),
|
||||||
|
type: 'incoming',
|
||||||
|
timestamp: 123,
|
||||||
|
sent_at: 123,
|
||||||
|
conversationId: group.id,
|
||||||
|
sourceServiceId: contactA.attributes.serviceId,
|
||||||
|
source: contactA.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches on another recipient's reaction", async () => {
|
||||||
|
assert.isTrue(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: contactA.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: contactB.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does not matches if sender is not in group', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: contactA.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: contactC.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('outgoing 1:1 story', () => {
|
||||||
|
let message: MessageAttributesType;
|
||||||
|
beforeEach(() => {
|
||||||
|
message = {
|
||||||
|
...generateMessageId(incrementMessageCounter()),
|
||||||
|
type: 'story',
|
||||||
|
timestamp: 123,
|
||||||
|
sent_at: 123,
|
||||||
|
conversationId: contactA.id,
|
||||||
|
sourceServiceId: ourConversation.attributes.serviceId,
|
||||||
|
source: ourConversation.id,
|
||||||
|
sendStateByConversationId: {
|
||||||
|
[contactA.id]: {
|
||||||
|
status: SendStatus.Sent,
|
||||||
|
isAllowedToReplyToStory: true,
|
||||||
|
},
|
||||||
|
[contactB.id]: {
|
||||||
|
status: SendStatus.Sent,
|
||||||
|
isAllowedToReplyToStory: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
it('allows reactions from those allowed to react', async () => {
|
||||||
|
assert.isTrue(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: OUR_ACI,
|
||||||
|
reactionSenderConversationId: contactA.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does not allow reactions from those disallowed from reacting', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: OUR_ACI,
|
||||||
|
reactionSenderConversationId: contactB.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does not allow reactions from non-recipients', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: OUR_ACI,
|
||||||
|
reactionSenderConversationId: contactC.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('incoming 1:1 story', () => {
|
||||||
|
let message: MessageAttributesType;
|
||||||
|
beforeEach(() => {
|
||||||
|
message = {
|
||||||
|
...generateMessageId(incrementMessageCounter()),
|
||||||
|
type: 'story',
|
||||||
|
timestamp: 123,
|
||||||
|
sent_at: 123,
|
||||||
|
conversationId: contactA.id,
|
||||||
|
sourceServiceId: contactA.attributes.serviceId,
|
||||||
|
source: contactA.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
it('allows reactions from self', async () => {
|
||||||
|
assert.isTrue(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: contactA.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: ourConversation.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does not allow reactions from others', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: contactA.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: contactB.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('outgoing group story', () => {
|
||||||
|
let message: MessageAttributesType;
|
||||||
|
beforeEach(() => {
|
||||||
|
message = {
|
||||||
|
...generateMessageId(incrementMessageCounter()),
|
||||||
|
type: 'story',
|
||||||
|
timestamp: 123,
|
||||||
|
sent_at: 123,
|
||||||
|
conversationId: contactA.id,
|
||||||
|
sourceServiceId: ourConversation.attributes.serviceId,
|
||||||
|
source: ourConversation.id,
|
||||||
|
sendStateByConversationId: {
|
||||||
|
[contactA.id]: {
|
||||||
|
status: SendStatus.Sent,
|
||||||
|
isAllowedToReplyToStory: true,
|
||||||
|
},
|
||||||
|
[contactB.id]: {
|
||||||
|
status: SendStatus.Sent,
|
||||||
|
isAllowedToReplyToStory: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
it('allows reactions from those allowed to react', async () => {
|
||||||
|
assert.isTrue(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: OUR_ACI,
|
||||||
|
reactionSenderConversationId: contactA.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does not allow reactions from those disallowed from reacting', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: OUR_ACI,
|
||||||
|
reactionSenderConversationId: contactB.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does not allow reactions from non-recipients', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: OUR_ACI,
|
||||||
|
reactionSenderConversationId: contactC.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('incoming group story message', () => {
|
||||||
|
let message: MessageAttributesType;
|
||||||
|
let group: ConversationModel;
|
||||||
|
beforeEach(() => {
|
||||||
|
group = window.ConversationController.getOrCreate(
|
||||||
|
generateGuid(),
|
||||||
|
'group',
|
||||||
|
{
|
||||||
|
groupVersion: 2,
|
||||||
|
membersV2: [
|
||||||
|
{
|
||||||
|
aci: contactA.getCheckedAci(''),
|
||||||
|
joinedAtVersion: 2,
|
||||||
|
role: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
aci: contactB.getCheckedAci(''),
|
||||||
|
joinedAtVersion: 2,
|
||||||
|
role: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
aci: OUR_ACI,
|
||||||
|
joinedAtVersion: 2,
|
||||||
|
role: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
message = {
|
||||||
|
...generateMessageId(incrementMessageCounter()),
|
||||||
|
type: 'story',
|
||||||
|
timestamp: 123,
|
||||||
|
sent_at: 123,
|
||||||
|
conversationId: group.id,
|
||||||
|
sourceServiceId: contactA.attributes.serviceId,
|
||||||
|
source: contactA.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches on another recipient's reaction", async () => {
|
||||||
|
assert.isTrue(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: contactA.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: contactB.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('does not matches if sender is not in group', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message,
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: contactA.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: contactC.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('other message types', () => {
|
||||||
|
it('does not match on other message types', async () => {
|
||||||
|
assert.isFalse(
|
||||||
|
isMessageAMatchForReaction({
|
||||||
|
message: {
|
||||||
|
...generateMessageId(incrementMessageCounter()),
|
||||||
|
type: 'verified-change',
|
||||||
|
timestamp: 123,
|
||||||
|
sent_at: 123,
|
||||||
|
conversationId: contactA.id,
|
||||||
|
sourceServiceId: ourConversation.attributes.serviceId,
|
||||||
|
source: ourConversation.id,
|
||||||
|
},
|
||||||
|
targetTimestamp: 123,
|
||||||
|
targetAuthorAci: ourConversation.getCheckedAci(''),
|
||||||
|
reactionSenderConversationId: contactA.id,
|
||||||
|
ourAci: OUR_ACI,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue