Group name spoofing warning
This commit is contained in:
parent
51b45ab275
commit
36c15fead4
20 changed files with 1312 additions and 215 deletions
|
@ -1,11 +1,12 @@
|
|||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { pick } from 'lodash';
|
||||
import { isEmpty, mapValues, pick } from 'lodash';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import {
|
||||
ContactSpoofingReviewPropType,
|
||||
Timeline,
|
||||
WarningType as TimelineWarningType,
|
||||
} from '../../components/conversation/Timeline';
|
||||
|
@ -14,6 +15,7 @@ import { ConversationType } from '../ducks/conversations';
|
|||
|
||||
import { getIntl } from '../selectors/user';
|
||||
import {
|
||||
getConversationByIdSelector,
|
||||
getConversationMessagesSelector,
|
||||
getConversationSelector,
|
||||
getConversationsByTitleSelector,
|
||||
|
@ -29,7 +31,16 @@ import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
|||
import { renderAudioAttachment } from './renderAudioAttachment';
|
||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
import { assert } from '../../util/assert';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
||||
import {
|
||||
dehydrateCollisionsWithConversations,
|
||||
getCollisionsFromMemberships,
|
||||
invertIdsByTitle,
|
||||
} from '../../util/groupMemberNameCollisions';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||
|
||||
// Workaround: A react component's required properties are filtering up through connect()
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||
|
@ -90,54 +101,117 @@ const getWarning = (
|
|||
conversation: Readonly<ConversationType>,
|
||||
state: Readonly<StateType>
|
||||
): undefined | TimelineWarningType => {
|
||||
if (
|
||||
conversation.type === 'direct' &&
|
||||
!conversation.acceptedMessageRequest &&
|
||||
!conversation.isBlocked
|
||||
) {
|
||||
const getConversationsWithTitle = getConversationsByTitleSelector(state);
|
||||
const conversationsWithSameTitle = getConversationsWithTitle(
|
||||
conversation.title
|
||||
);
|
||||
assert(
|
||||
conversationsWithSameTitle.length,
|
||||
'Expected at least 1 conversation with the same title (this one)'
|
||||
);
|
||||
switch (conversation.type) {
|
||||
case 'direct':
|
||||
if (!conversation.acceptedMessageRequest && !conversation.isBlocked) {
|
||||
const getConversationsWithTitle = getConversationsByTitleSelector(
|
||||
state
|
||||
);
|
||||
const conversationsWithSameTitle = getConversationsWithTitle(
|
||||
conversation.title
|
||||
);
|
||||
assert(
|
||||
conversationsWithSameTitle.length,
|
||||
'Expected at least 1 conversation with the same title (this one)'
|
||||
);
|
||||
|
||||
const safeConversation = conversationsWithSameTitle.find(
|
||||
otherConversation =>
|
||||
otherConversation.acceptedMessageRequest &&
|
||||
otherConversation.type === 'direct' &&
|
||||
otherConversation.id !== conversation.id
|
||||
);
|
||||
const safeConversation = conversationsWithSameTitle.find(
|
||||
otherConversation =>
|
||||
otherConversation.acceptedMessageRequest &&
|
||||
otherConversation.type === 'direct' &&
|
||||
otherConversation.id !== conversation.id
|
||||
);
|
||||
|
||||
return safeConversation ? { safeConversation } : undefined;
|
||||
if (safeConversation) {
|
||||
return {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
safeConversation,
|
||||
};
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
case 'group': {
|
||||
if (conversation.left || conversation.groupVersion !== 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const getConversationById = getConversationByIdSelector(state);
|
||||
|
||||
const { memberships } = getGroupMemberships(
|
||||
conversation,
|
||||
getConversationById
|
||||
);
|
||||
const groupNameCollisions = getCollisionsFromMemberships(memberships);
|
||||
const hasGroupMembersWithSameName = !isEmpty(groupNameCollisions);
|
||||
if (hasGroupMembersWithSameName) {
|
||||
return {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
acknowledgedGroupNameCollisions:
|
||||
conversation.acknowledgedGroupNameCollisions || {},
|
||||
groupNameCollisions: dehydrateCollisionsWithConversations(
|
||||
groupNameCollisions
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
default:
|
||||
throw missingCaseError(conversation.type);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getContactSpoofingReview = (
|
||||
selectedConversationId: string,
|
||||
state: Readonly<StateType>
|
||||
):
|
||||
| undefined
|
||||
| {
|
||||
possiblyUnsafeConversation: ConversationType;
|
||||
safeConversation: ConversationType;
|
||||
} => {
|
||||
): undefined | ContactSpoofingReviewPropType => {
|
||||
const { contactSpoofingReview } = state.conversations;
|
||||
if (!contactSpoofingReview) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const conversationSelector = getConversationSelector(state);
|
||||
return {
|
||||
possiblyUnsafeConversation: conversationSelector(selectedConversationId),
|
||||
safeConversation: conversationSelector(
|
||||
contactSpoofingReview.safeConversationId
|
||||
),
|
||||
};
|
||||
const getConversationById = getConversationByIdSelector(state);
|
||||
|
||||
const currentConversation = conversationSelector(selectedConversationId);
|
||||
|
||||
switch (contactSpoofingReview.type) {
|
||||
case ContactSpoofingType.DirectConversationWithSameTitle:
|
||||
return {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
possiblyUnsafeConversation: currentConversation,
|
||||
safeConversation: conversationSelector(
|
||||
contactSpoofingReview.safeConversationId
|
||||
),
|
||||
};
|
||||
case ContactSpoofingType.MultipleGroupMembersWithSameTitle: {
|
||||
const { memberships } = getGroupMemberships(
|
||||
currentConversation,
|
||||
getConversationById
|
||||
);
|
||||
const groupNameCollisions = getCollisionsFromMemberships(memberships);
|
||||
|
||||
const previouslyAcknowledgedTitlesById = invertIdsByTitle(
|
||||
currentConversation.acknowledgedGroupNameCollisions || {}
|
||||
);
|
||||
|
||||
const collisionInfoByTitle = mapValues(
|
||||
groupNameCollisions,
|
||||
conversations =>
|
||||
conversations.map(conversation => ({
|
||||
conversation,
|
||||
oldName: getOwn(previouslyAcknowledgedTitlesById, conversation.id),
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
collisionInfoByTitle,
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw missingCaseError(contactSpoofingReview);
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
|
@ -150,6 +224,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
return {
|
||||
id,
|
||||
...pick(conversation, [
|
||||
'areWeAdmin',
|
||||
'unreadCount',
|
||||
'typingContact',
|
||||
'isGroupV1AndDisabled',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue