2022-02-23 18:48:40 +00:00
|
|
|
// Copyright 2019-2022 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-09-23 14:26:25 +00:00
|
|
|
import { omit } from 'lodash';
|
|
|
|
|
|
|
|
import { SignalService as Proto } from '../protobuf';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { MessageAttributesType } from '../model-types.d';
|
2021-09-23 14:26:25 +00:00
|
|
|
|
|
|
|
import { isNotNil } from '../util/isNotNil';
|
|
|
|
import {
|
|
|
|
format as formatPhoneNumber,
|
|
|
|
parse as parsePhoneNumber,
|
|
|
|
} from './PhoneNumber';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { AttachmentType, migrateDataToFileSystem } from './Attachment';
|
2021-09-23 14:26:25 +00:00
|
|
|
import { toLogFormat } from './errors';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { LoggerType } from './Logging';
|
2022-04-12 00:26:09 +00:00
|
|
|
import type { UUIDStringType } from './UUID';
|
2018-05-05 01:19:54 +00:00
|
|
|
|
2021-08-20 01:56:39 +00:00
|
|
|
export type EmbeddedContactType = {
|
2018-05-09 15:55:18 +00:00
|
|
|
name?: Name;
|
2018-05-05 01:19:54 +00:00
|
|
|
number?: Array<Phone>;
|
|
|
|
email?: Array<Email>;
|
|
|
|
address?: Array<PostalAddress>;
|
|
|
|
avatar?: Avatar;
|
|
|
|
organization?: string;
|
2021-06-17 17:15:10 +00:00
|
|
|
|
|
|
|
// Populated by selector
|
|
|
|
firstNumber?: string;
|
2022-04-12 00:26:09 +00:00
|
|
|
uuid?: UUIDStringType;
|
2021-01-14 18:07:05 +00:00
|
|
|
};
|
2018-05-05 01:19:54 +00:00
|
|
|
|
2021-01-14 18:07:05 +00:00
|
|
|
type Name = {
|
2018-05-05 01:19:54 +00:00
|
|
|
givenName?: string;
|
|
|
|
familyName?: string;
|
|
|
|
prefix?: string;
|
|
|
|
suffix?: string;
|
|
|
|
middleName?: string;
|
2018-05-08 23:53:18 +00:00
|
|
|
displayName?: string;
|
2021-01-14 18:07:05 +00:00
|
|
|
};
|
2018-05-05 01:19:54 +00:00
|
|
|
|
2019-03-15 22:18:00 +00:00
|
|
|
export enum ContactFormType {
|
2018-05-05 01:19:54 +00:00
|
|
|
HOME = 1,
|
|
|
|
MOBILE = 2,
|
|
|
|
WORK = 3,
|
|
|
|
CUSTOM = 4,
|
|
|
|
}
|
|
|
|
|
|
|
|
export enum AddressType {
|
|
|
|
HOME = 1,
|
|
|
|
WORK = 2,
|
|
|
|
CUSTOM = 3,
|
|
|
|
}
|
|
|
|
|
2021-01-14 18:07:05 +00:00
|
|
|
export type Phone = {
|
2018-05-05 01:19:54 +00:00
|
|
|
value: string;
|
2019-03-15 22:18:00 +00:00
|
|
|
type: ContactFormType;
|
2018-05-05 01:19:54 +00:00
|
|
|
label?: string;
|
2021-01-14 18:07:05 +00:00
|
|
|
};
|
2018-05-05 01:19:54 +00:00
|
|
|
|
2021-01-14 18:07:05 +00:00
|
|
|
export type Email = {
|
2018-05-05 01:19:54 +00:00
|
|
|
value: string;
|
2019-03-15 22:18:00 +00:00
|
|
|
type: ContactFormType;
|
2018-05-05 01:19:54 +00:00
|
|
|
label?: string;
|
2021-01-14 18:07:05 +00:00
|
|
|
};
|
2018-05-05 01:19:54 +00:00
|
|
|
|
2021-01-14 18:07:05 +00:00
|
|
|
export type PostalAddress = {
|
2018-05-05 01:19:54 +00:00
|
|
|
type: AddressType;
|
|
|
|
label?: string;
|
|
|
|
street?: string;
|
|
|
|
pobox?: string;
|
|
|
|
neighborhood?: string;
|
|
|
|
city?: string;
|
|
|
|
region?: string;
|
|
|
|
postcode?: string;
|
|
|
|
country?: string;
|
2021-01-14 18:07:05 +00:00
|
|
|
};
|
2018-05-05 01:19:54 +00:00
|
|
|
|
2021-09-23 14:26:25 +00:00
|
|
|
export type Avatar = {
|
2021-06-17 17:15:10 +00:00
|
|
|
avatar: AttachmentType;
|
2018-05-05 01:19:54 +00:00
|
|
|
isProfile: boolean;
|
2021-01-14 18:07:05 +00:00
|
|
|
};
|
2018-05-05 01:19:54 +00:00
|
|
|
|
2021-09-23 14:26:25 +00:00
|
|
|
const DEFAULT_PHONE_TYPE = Proto.DataMessage.Contact.Phone.Type.HOME;
|
|
|
|
const DEFAULT_EMAIL_TYPE = Proto.DataMessage.Contact.Email.Type.HOME;
|
|
|
|
const DEFAULT_ADDRESS_TYPE = Proto.DataMessage.Contact.PostalAddress.Type.HOME;
|
|
|
|
|
2022-04-11 20:57:44 +00:00
|
|
|
export function numberToPhoneType(
|
|
|
|
type: number
|
|
|
|
): Proto.DataMessage.Contact.Phone.Type {
|
|
|
|
if (type === Proto.DataMessage.Contact.Phone.Type.MOBILE) {
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
if (type === Proto.DataMessage.Contact.Phone.Type.WORK) {
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
if (type === Proto.DataMessage.Contact.Phone.Type.CUSTOM) {
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
|
|
|
|
return DEFAULT_PHONE_TYPE;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function numberToEmailType(
|
|
|
|
type: number
|
|
|
|
): Proto.DataMessage.Contact.Email.Type {
|
|
|
|
if (type === Proto.DataMessage.Contact.Email.Type.MOBILE) {
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
if (type === Proto.DataMessage.Contact.Email.Type.WORK) {
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
if (type === Proto.DataMessage.Contact.Email.Type.CUSTOM) {
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
|
|
|
|
return DEFAULT_EMAIL_TYPE;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function numberToAddressType(
|
|
|
|
type: number
|
|
|
|
): Proto.DataMessage.Contact.PostalAddress.Type {
|
|
|
|
if (type === Proto.DataMessage.Contact.PostalAddress.Type.WORK) {
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
if (type === Proto.DataMessage.Contact.PostalAddress.Type.CUSTOM) {
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
|
|
|
|
return DEFAULT_ADDRESS_TYPE;
|
|
|
|
}
|
|
|
|
|
2021-08-20 01:56:39 +00:00
|
|
|
export function embeddedContactSelector(
|
|
|
|
contact: EmbeddedContactType,
|
2018-05-05 01:19:54 +00:00
|
|
|
options: {
|
2022-02-23 18:48:40 +00:00
|
|
|
regionCode?: string;
|
2021-06-17 17:15:10 +00:00
|
|
|
firstNumber?: string;
|
2022-04-12 00:26:09 +00:00
|
|
|
uuid?: UUIDStringType;
|
2018-05-05 01:19:54 +00:00
|
|
|
getAbsoluteAttachmentPath: (path: string) => string;
|
|
|
|
}
|
2021-08-20 01:56:39 +00:00
|
|
|
): EmbeddedContactType {
|
2022-04-12 00:26:09 +00:00
|
|
|
const { getAbsoluteAttachmentPath, firstNumber, uuid, regionCode } = options;
|
2018-05-05 01:19:54 +00:00
|
|
|
|
|
|
|
let { avatar } = contact;
|
2019-01-30 20:15:07 +00:00
|
|
|
if (avatar && avatar.avatar) {
|
|
|
|
if (avatar.avatar.error) {
|
|
|
|
avatar = undefined;
|
|
|
|
} else {
|
|
|
|
avatar = {
|
|
|
|
...avatar,
|
|
|
|
avatar: {
|
|
|
|
...avatar.avatar,
|
|
|
|
path: avatar.avatar.path
|
|
|
|
? getAbsoluteAttachmentPath(avatar.avatar.path)
|
|
|
|
: undefined,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2018-05-05 01:19:54 +00:00
|
|
|
}
|
2018-05-22 19:31:43 +00:00
|
|
|
|
2018-05-08 16:57:51 +00:00
|
|
|
return {
|
|
|
|
...contact,
|
2021-06-17 17:15:10 +00:00
|
|
|
firstNumber,
|
2022-04-12 00:26:09 +00:00
|
|
|
uuid,
|
2018-05-05 01:19:54 +00:00
|
|
|
avatar,
|
|
|
|
number:
|
|
|
|
contact.number &&
|
|
|
|
contact.number.map(item => ({
|
|
|
|
...item,
|
|
|
|
value: formatPhoneNumber(item.value, {
|
|
|
|
ourRegionCode: regionCode,
|
|
|
|
}),
|
|
|
|
})),
|
2018-05-08 16:57:51 +00:00
|
|
|
};
|
2018-05-05 01:19:54 +00:00
|
|
|
}
|
2018-05-07 22:44:19 +00:00
|
|
|
|
2021-08-20 01:56:39 +00:00
|
|
|
export function getName(contact: EmbeddedContactType): string | undefined {
|
2018-05-07 22:44:19 +00:00
|
|
|
const { name, organization } = contact;
|
2019-01-14 21:49:58 +00:00
|
|
|
const displayName = (name && name.displayName) || undefined;
|
|
|
|
const givenName = (name && name.givenName) || undefined;
|
|
|
|
const familyName = (name && name.familyName) || undefined;
|
2018-05-08 23:53:18 +00:00
|
|
|
const backupName =
|
2019-01-14 21:49:58 +00:00
|
|
|
(givenName && familyName && `${givenName} ${familyName}`) || undefined;
|
2018-05-08 23:53:18 +00:00
|
|
|
|
|
|
|
return displayName || organization || backupName || givenName || familyName;
|
2018-05-07 22:44:19 +00:00
|
|
|
}
|
2021-09-23 14:26:25 +00:00
|
|
|
|
|
|
|
export function parseAndWriteAvatar(
|
|
|
|
upgradeAttachment: typeof migrateDataToFileSystem
|
|
|
|
) {
|
|
|
|
return async (
|
|
|
|
contact: EmbeddedContactType,
|
|
|
|
context: {
|
|
|
|
message: MessageAttributesType;
|
2022-06-10 01:10:20 +00:00
|
|
|
getRegionCode: () => string | undefined;
|
2021-09-23 14:26:25 +00:00
|
|
|
logger: Pick<LoggerType, 'error'>;
|
2021-09-24 00:49:05 +00:00
|
|
|
writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
|
2021-09-23 14:26:25 +00:00
|
|
|
}
|
|
|
|
): Promise<EmbeddedContactType> => {
|
2022-06-10 01:10:20 +00:00
|
|
|
const { message, getRegionCode, logger } = context;
|
2021-09-23 14:26:25 +00:00
|
|
|
const { avatar } = contact;
|
|
|
|
|
|
|
|
const contactWithUpdatedAvatar =
|
|
|
|
avatar && avatar.avatar
|
|
|
|
? {
|
|
|
|
...contact,
|
|
|
|
avatar: {
|
|
|
|
...avatar,
|
|
|
|
avatar: await upgradeAttachment(avatar.avatar, context),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
: omit(contact, ['avatar']);
|
|
|
|
|
|
|
|
// eliminates empty numbers, emails, and addresses; adds type if not provided
|
|
|
|
const parsedContact = parseContact(contactWithUpdatedAvatar, {
|
2022-06-10 01:10:20 +00:00
|
|
|
regionCode: getRegionCode(),
|
2021-09-23 14:26:25 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const error = _validate(parsedContact, {
|
|
|
|
messageId: idForLogging(message),
|
|
|
|
});
|
|
|
|
if (error) {
|
|
|
|
logger.error(
|
|
|
|
'parseAndWriteAvatar: contact was malformed.',
|
|
|
|
toLogFormat(error)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return parsedContact;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseContact(
|
|
|
|
contact: EmbeddedContactType,
|
2022-06-10 01:10:20 +00:00
|
|
|
{ regionCode }: { regionCode: string | undefined }
|
2021-09-23 14:26:25 +00:00
|
|
|
): EmbeddedContactType {
|
|
|
|
const boundParsePhone = (phoneNumber: Phone): Phone | undefined =>
|
|
|
|
parsePhoneItem(phoneNumber, { regionCode });
|
|
|
|
|
|
|
|
const skipEmpty = <T>(arr: Array<T | undefined>): Array<T> | undefined => {
|
|
|
|
const filtered: Array<T> = arr.filter(isNotNil);
|
|
|
|
return filtered.length ? filtered : undefined;
|
|
|
|
};
|
|
|
|
|
|
|
|
const number = skipEmpty((contact.number || []).map(boundParsePhone));
|
|
|
|
const email = skipEmpty((contact.email || []).map(parseEmailItem));
|
|
|
|
const address = skipEmpty((contact.address || []).map(parseAddress));
|
|
|
|
|
|
|
|
let result = {
|
|
|
|
...omit(contact, ['avatar', 'number', 'email', 'address']),
|
|
|
|
...parseAvatar(contact.avatar),
|
|
|
|
};
|
|
|
|
|
|
|
|
if (number) {
|
|
|
|
result = { ...result, number };
|
|
|
|
}
|
|
|
|
if (email) {
|
|
|
|
result = { ...result, email };
|
|
|
|
}
|
|
|
|
if (address) {
|
|
|
|
result = { ...result, address };
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function idForLogging(message: MessageAttributesType): string {
|
|
|
|
return `${message.source}.${message.sourceDevice} ${message.sent_at}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Exported for testing
|
|
|
|
export function _validate(
|
|
|
|
contact: EmbeddedContactType,
|
|
|
|
{ messageId }: { messageId: string }
|
|
|
|
): Error | undefined {
|
|
|
|
const { name, number, email, address, organization } = contact;
|
|
|
|
|
|
|
|
if ((!name || !name.displayName) && !organization) {
|
|
|
|
return new Error(
|
|
|
|
`Message ${messageId}: Contact had neither 'displayName' nor 'organization'`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
(!number || !number.length) &&
|
|
|
|
(!email || !email.length) &&
|
|
|
|
(!address || !address.length)
|
|
|
|
) {
|
|
|
|
return new Error(
|
|
|
|
`Message ${messageId}: Contact had no included numbers, email or addresses`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parsePhoneItem(
|
|
|
|
item: Phone,
|
2022-06-10 01:10:20 +00:00
|
|
|
{ regionCode }: { regionCode: string | undefined }
|
2021-09-23 14:26:25 +00:00
|
|
|
): Phone | undefined {
|
|
|
|
if (!item.value) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...item,
|
|
|
|
type: item.type || DEFAULT_PHONE_TYPE,
|
|
|
|
value: parsePhoneNumber(item.value, { regionCode }),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseEmailItem(item: Email): Email | undefined {
|
|
|
|
if (!item.value) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return { ...item, type: item.type || DEFAULT_EMAIL_TYPE };
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseAddress(address: PostalAddress): PostalAddress | undefined {
|
|
|
|
if (!address) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
!address.street &&
|
|
|
|
!address.pobox &&
|
|
|
|
!address.neighborhood &&
|
|
|
|
!address.city &&
|
|
|
|
!address.region &&
|
|
|
|
!address.postcode &&
|
|
|
|
!address.country
|
|
|
|
) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return { ...address, type: address.type || DEFAULT_ADDRESS_TYPE };
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseAvatar(avatar?: Avatar): { avatar: Avatar } | undefined {
|
|
|
|
if (!avatar) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
avatar: {
|
|
|
|
...avatar,
|
|
|
|
isProfile: avatar.isProfile || false,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|