Group name spoofing warning
This commit is contained in:
parent
51b45ab275
commit
36c15fead4
20 changed files with 1312 additions and 215 deletions
7
ts/util/contactSpoofing.ts
Normal file
7
ts/util/contactSpoofing.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export enum ContactSpoofingType {
|
||||
DirectConversationWithSameTitle,
|
||||
MultipleGroupMembersWithSameTitle,
|
||||
}
|
68
ts/util/groupMemberNameCollisions.ts
Normal file
68
ts/util/groupMemberNameCollisions.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { mapValues, pickBy } from 'lodash';
|
||||
import { groupBy, map, filter } from './iterables';
|
||||
import { getOwn } from './getOwn';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { isConversationNameKnown } from './isConversationNameKnown';
|
||||
|
||||
export type GroupNameCollisionsWithIdsByTitle = Record<string, Array<string>>;
|
||||
export type GroupNameCollisionsWithConversationsByTitle = Record<
|
||||
string,
|
||||
Array<ConversationType>
|
||||
>;
|
||||
export type GroupNameCollisionsWithTitlesById = Record<string, string>;
|
||||
|
||||
export const dehydrateCollisionsWithConversations = (
|
||||
withConversations: Readonly<GroupNameCollisionsWithConversationsByTitle>
|
||||
): GroupNameCollisionsWithIdsByTitle =>
|
||||
mapValues(withConversations, conversations => conversations.map(c => c.id));
|
||||
|
||||
export function getCollisionsFromMemberships(
|
||||
memberships: Iterable<{ member: ConversationType }>
|
||||
): GroupNameCollisionsWithConversationsByTitle {
|
||||
const members = map(memberships, membership => membership.member);
|
||||
const candidateMembers = filter(
|
||||
members,
|
||||
member => !member.isMe && isConversationNameKnown(member)
|
||||
);
|
||||
const groupedByTitle = groupBy(candidateMembers, member => member.title);
|
||||
// This cast is here because `pickBy` returns a `Partial`, which is incompatible with
|
||||
// `Record`. [This demonstates the problem][0], but I don't believe it's an actual
|
||||
// issue in the code.
|
||||
//
|
||||
// Alternatively, we could filter undefined keys or something like that.
|
||||
//
|
||||
// [0]: https://www.typescriptlang.org/play?#code/C4TwDgpgBAYg9nKBeKAFAhgJ2AS3QGwB4AlCAYzkwBNCBnYTHAOwHMAaKJgVwFsAjCJgB8QgNwAoCk3pQAZgC5YCZFADeUABY5FAVigBfCeNCQoAISwrSFanQbN2nXgOESpMvoouYVs0UA
|
||||
return (pickBy(
|
||||
groupedByTitle,
|
||||
group => group.length >= 2
|
||||
) as unknown) as GroupNameCollisionsWithConversationsByTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the user should see a group member name collision warning, and
|
||||
* `false` otherwise. Users should see these warnings if any collisions appear that they
|
||||
* haven't dismissed.
|
||||
*/
|
||||
export const hasUnacknowledgedCollisions = (
|
||||
previous: Readonly<GroupNameCollisionsWithIdsByTitle>,
|
||||
current: Readonly<GroupNameCollisionsWithIdsByTitle>
|
||||
): boolean =>
|
||||
Object.entries(current).some(([title, currentIds]) => {
|
||||
const previousIds = new Set(getOwn(previous, title) || []);
|
||||
return currentIds.some(currentId => !previousIds.has(currentId));
|
||||
});
|
||||
|
||||
export const invertIdsByTitle = (
|
||||
idsByTitle: Readonly<GroupNameCollisionsWithIdsByTitle>
|
||||
): GroupNameCollisionsWithTitlesById => {
|
||||
const result: GroupNameCollisionsWithTitlesById = Object.create(null);
|
||||
Object.entries(idsByTitle).forEach(([title, ids]) => {
|
||||
ids.forEach(id => {
|
||||
result[id] = title;
|
||||
});
|
||||
});
|
||||
return result;
|
||||
};
|
22
ts/util/isConversationNameKnown.ts
Normal file
22
ts/util/isConversationNameKnown.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { missingCaseError } from './missingCaseError';
|
||||
|
||||
export function isConversationNameKnown(
|
||||
conversation: Readonly<
|
||||
Pick<ConversationType, 'e164' | 'name' | 'profileName' | 'type'>
|
||||
>
|
||||
): boolean {
|
||||
switch (conversation.type) {
|
||||
case 'direct':
|
||||
return Boolean(
|
||||
conversation.name || conversation.profileName || conversation.e164
|
||||
);
|
||||
case 'group':
|
||||
return Boolean(conversation.name);
|
||||
default:
|
||||
throw missingCaseError(conversation.type);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
|
||||
import { getOwn } from './getOwn';
|
||||
|
||||
export function isIterable(value: unknown): value is Iterable<unknown> {
|
||||
return (
|
||||
(typeof value === 'object' && value !== null && Symbol.iterator in value) ||
|
||||
|
@ -88,6 +90,23 @@ class FilterIterator<T> implements Iterator<T> {
|
|||
}
|
||||
}
|
||||
|
||||
export function groupBy<T>(
|
||||
iterable: Iterable<T>,
|
||||
fn: (value: T) => string
|
||||
): Record<string, Array<T>> {
|
||||
const result: Record<string, Array<T>> = Object.create(null);
|
||||
for (const value of iterable) {
|
||||
const key = fn(value);
|
||||
const existingGroup = getOwn(result, key);
|
||||
if (existingGroup) {
|
||||
existingGroup.push(value);
|
||||
} else {
|
||||
result[key] = [value];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function map<T, ResultT>(
|
||||
iterable: Iterable<T>,
|
||||
fn: (value: T) => ResultT
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue