Use UUIDs in group database schema

This commit is contained in:
Fedor Indutny 2021-10-26 15:59:08 -07:00 committed by GitHub
parent 74fde10ff5
commit 63fcdbe787
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 4530 additions and 3664 deletions

View file

@ -378,14 +378,12 @@ try {
}); });
const { imageToBlurHash } = require('./ts/util/imageToBlurHash'); const { imageToBlurHash } = require('./ts/util/imageToBlurHash');
const { isValidGuid } = require('./ts/util/isValidGuid');
const { ActiveWindowService } = require('./ts/services/ActiveWindowService'); const { ActiveWindowService } = require('./ts/services/ActiveWindowService');
window.imageToBlurHash = imageToBlurHash; window.imageToBlurHash = imageToBlurHash;
window.emojiData = require('emoji-datasource'); window.emojiData = require('emoji-datasource');
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance(); window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat; window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
window.getGuid = require('uuid/v4');
const activeWindowService = new ActiveWindowService(); const activeWindowService = new ActiveWindowService();
activeWindowService.initialize(window.document, ipc); activeWindowService.initialize(window.document, ipc);
@ -401,8 +399,6 @@ try {
reducedMotionSetting: Boolean(config.reducedMotionSetting), reducedMotionSetting: Boolean(config.reducedMotionSetting),
}; };
window.isValidGuid = isValidGuid;
window.React = require('react'); window.React = require('react');
window.ReactDOM = require('react-dom'); window.ReactDOM = require('react-dom');
window.moment = require('moment'); window.moment = require('moment');

View file

@ -34,7 +34,6 @@ const MAX_ANIMATED_STICKER_BYTE_LENGTH = 300 * 1024;
window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/'; window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/';
window.getEnvironment = getEnvironment; window.getEnvironment = getEnvironment;
window.getVersion = () => config.version; window.getVersion = () => config.version;
window.getGuid = require('uuid/v4');
window.PQueue = require('p-queue').default; window.PQueue = require('p-queue').default;
window.Backbone = require('backbone'); window.Backbone = require('backbone');

View file

@ -8,7 +8,6 @@ const chaiAsPromised = require('chai-as-promised');
const { Crypto } = require('../ts/context/Crypto'); const { Crypto } = require('../ts/context/Crypto');
const { setEnvironment, Environment } = require('../ts/environment'); const { setEnvironment, Environment } = require('../ts/environment');
const { isValidGuid } = require('../ts/util/isValidGuid');
chai.use(chaiAsPromised); chai.use(chaiAsPromised);
@ -31,7 +30,6 @@ global.window = {
get: key => storageMap.get(key), get: key => storageMap.get(key),
put: async (key, value) => storageMap.set(key, value), put: async (key, value) => storageMap.set(key, value),
}, },
isValidGuid,
}; };
// For ducks/network.getEmptyState() // For ducks/network.getEmptyState()

View file

@ -13,10 +13,9 @@ import type {
import type { ConversationModel } from './models/conversations'; import type { ConversationModel } from './models/conversations';
import { maybeDeriveGroupV2Id } from './groups'; import { maybeDeriveGroupV2Id } from './groups';
import { assert } from './util/assert'; import { assert } from './util/assert';
import { isValidGuid } from './util/isValidGuid';
import { map, reduce } from './util/iterables'; import { map, reduce } from './util/iterables';
import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation'; import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation';
import { UUID } from './types/UUID'; import { UUID, isValidUuid } from './types/UUID';
import { Address } from './types/Address'; import { Address } from './types/Address';
import { QualifiedAddress } from './types/QualifiedAddress'; import { QualifiedAddress } from './types/QualifiedAddress';
import * as log from './logging/log'; import * as log from './logging/log';
@ -25,7 +24,7 @@ const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
const { const {
getAllConversations, getAllConversations,
getAllGroupsInvolvingId, getAllGroupsInvolvingUuid,
getMessagesBySentAt, getMessagesBySentAt,
migrateConversationMessages, migrateConversationMessages,
removeConversation, removeConversation,
@ -181,7 +180,7 @@ export class ConversationController {
return conversation; return conversation;
} }
const id = window.getGuid(); const id = UUID.generate().toString();
if (type === 'group') { if (type === 'group') {
conversation = this._conversations.add({ conversation = this._conversations.add({
@ -193,7 +192,7 @@ export class ConversationController {
version: 2, version: 2,
...additionalInitialProps, ...additionalInitialProps,
}); });
} else if (window.isValidGuid(identifier)) { } else if (isValidUuid(identifier)) {
conversation = this._conversations.add({ conversation = this._conversations.add({
id, id,
uuid: identifier, uuid: identifier,
@ -617,7 +616,7 @@ export class ConversationController {
} }
const obsoleteId = obsolete.get('id'); const obsoleteId = obsolete.get('id');
const obsoleteUuid = obsolete.get('uuid'); const obsoleteUuid = obsolete.getUuid();
const currentId = current.get('id'); const currentId = current.get('id');
log.warn('combineConversations: Combining two conversations', { log.warn('combineConversations: Combining two conversations', {
obsolete: obsoleteId, obsolete: obsoleteId,
@ -643,13 +642,13 @@ export class ConversationController {
const ourUuid = window.textsecure.storage.user.getCheckedUuid(); const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({ const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({
ourUuid, ourUuid,
identifier: obsoleteUuid, identifier: obsoleteUuid.toString(),
}); });
await Promise.all( await Promise.all(
deviceIds.map(async deviceId => { deviceIds.map(async deviceId => {
const addr = new QualifiedAddress( const addr = new QualifiedAddress(
ourUuid, ourUuid,
Address.create(obsoleteUuid, deviceId) new Address(obsoleteUuid, deviceId)
); );
await window.textsecure.storage.protocol.removeSession(addr); await window.textsecure.storage.protocol.removeSession(addr);
}) })
@ -661,14 +660,14 @@ export class ConversationController {
if (obsoleteUuid) { if (obsoleteUuid) {
await window.textsecure.storage.protocol.removeIdentityKey( await window.textsecure.storage.protocol.removeIdentityKey(
new UUID(obsoleteUuid) obsoleteUuid
); );
} }
log.warn( log.warn(
'combineConversations: Ensure that all V1 groups have new conversationId instead of old' 'combineConversations: Ensure that all V1 groups have new conversationId instead of old'
); );
const groups = await this.getAllGroupsInvolvingId(obsoleteId); const groups = await this.getAllGroupsInvolvingUuid(obsoleteUuid);
groups.forEach(group => { groups.forEach(group => {
const members = group.get('members'); const members = group.get('members');
const withoutObsolete = without(members, obsoleteId); const withoutObsolete = without(members, obsoleteId);
@ -737,10 +736,10 @@ export class ConversationController {
return null; return null;
} }
async getAllGroupsInvolvingId( async getAllGroupsInvolvingUuid(
conversationId: string uuid: UUID
): Promise<Array<ConversationModel>> { ): Promise<Array<ConversationModel>> {
const groups = await getAllGroupsInvolvingId(conversationId, { const groups = await getAllGroupsInvolvingUuid(uuid.toString(), {
ConversationCollection: window.Whisper.ConversationCollection, ConversationCollection: window.Whisper.ConversationCollection,
}); });
return groups.map(group => { return groups.map(group => {
@ -836,7 +835,7 @@ export class ConversationController {
// Clean up the conversations that have UUID as their e164. // Clean up the conversations that have UUID as their e164.
const e164 = conversation.get('e164'); const e164 = conversation.get('e164');
const uuid = conversation.get('uuid'); const uuid = conversation.get('uuid');
if (isValidGuid(e164) && uuid) { if (isValidUuid(e164) && uuid) {
conversation.set({ e164: undefined }); conversation.set({ e164: undefined });
updateConversation(conversation.attributes); updateConversation(conversation.attributes);

View file

@ -12,6 +12,8 @@ import { calculateAgreement, generateKeyPair } from './Curve';
import * as log from './logging/log'; import * as log from './logging/log';
import { HashType, CipherType } from './types/Crypto'; import { HashType, CipherType } from './types/Crypto';
import { ProfileDecryptError } from './types/errors'; import { ProfileDecryptError } from './types/errors';
import { UUID } from './types/UUID';
import type { UUIDStringType } from './types/UUID';
export { HashType, CipherType }; export { HashType, CipherType };
@ -457,7 +459,7 @@ export function uuidToBytes(uuid: string): Uint8Array {
); );
} }
export function bytesToUuid(bytes: Uint8Array): undefined | string { export function bytesToUuid(bytes: Uint8Array): undefined | UUIDStringType {
if (bytes.byteLength !== 16) { if (bytes.byteLength !== 16) {
log.warn( log.warn(
'bytesToUuid: received an Uint8Array of invalid length. ' + 'bytesToUuid: received an Uint8Array of invalid length. ' +
@ -473,7 +475,7 @@ export function bytesToUuid(bytes: Uint8Array): undefined | string {
return undefined; return undefined;
} }
export function splitUuids(buffer: Uint8Array): Array<string | null> { export function splitUuids(buffer: Uint8Array): Array<UUIDStringType | null> {
const uuids = []; const uuids = [];
for (let i = 0; i < buffer.byteLength; i += 16) { for (let i = 0; i < buffer.byteLength; i += 16) {
const bytes = getBytes(buffer, i, 16); const bytes = getBytes(buffer, i, 16);
@ -487,7 +489,7 @@ export function splitUuids(buffer: Uint8Array): Array<string | null> {
]; ];
const uuid = chunks.join('-'); const uuid = chunks.join('-');
if (uuid !== '00000000-0000-0000-0000-000000000000') { if (uuid !== '00000000-0000-0000-0000-000000000000') {
uuids.push(uuid); uuids.push(UUID.cast(uuid));
} else { } else {
uuids.push(null); uuids.push(null);
} }

View file

@ -994,7 +994,7 @@ export class SignalProtocolStore extends EventsMixin {
id, id,
version: 2, version: 2,
ourUuid: qualifiedAddress.ourUuid.toString(), ourUuid: qualifiedAddress.ourUuid.toString(),
conversationId: new UUID(conversationId).toString(), conversationId,
uuid: uuid.toString(), uuid: uuid.toString(),
deviceId, deviceId,
record: record.serialize().toString('base64'), record: record.serialize().toString('base64'),
@ -1394,7 +1394,7 @@ export class SignalProtocolStore extends EventsMixin {
return undefined; return undefined;
} }
const conversationId = new UUID(conversation.id).toString(); const conversationId = conversation.id;
const record = this.identityKeys.get(`conversation:${conversationId}`); const record = this.identityKeys.get(`conversation:${conversationId}`);
if (!record) { if (!record) {
return undefined; return undefined;

View file

@ -3608,6 +3608,7 @@ export async function startApp(): Promise<void> {
messageSentAt: timestamp, messageSentAt: timestamp,
receiptTimestamp: envelopeTimestamp, receiptTimestamp: envelopeTimestamp,
sourceConversationId, sourceConversationId,
sourceUuid,
sourceDevice, sourceDevice,
type, type,
}); });
@ -3791,6 +3792,7 @@ export async function startApp(): Promise<void> {
messageSentAt: timestamp, messageSentAt: timestamp,
receiptTimestamp: envelopeTimestamp, receiptTimestamp: envelopeTimestamp,
sourceConversationId, sourceConversationId,
sourceUuid,
sourceDevice, sourceDevice,
type: MessageReceiptType.Delivery, type: MessageReceiptType.Delivery,
}); });

View file

@ -40,12 +40,13 @@ import type {
StartCallType, StartCallType,
} from '../state/ducks/calling'; } from '../state/ducks/calling';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import type { UUIDStringType } from '../types/UUID';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
const GROUP_CALL_RING_DURATION = 60 * 1000; const GROUP_CALL_RING_DURATION = 60 * 1000;
type MeType = ConversationType & { type MeType = ConversationType & {
uuid: string; uuid: UUIDStringType;
}; };
export type PropsType = { export type PropsType = {

View file

@ -3,7 +3,6 @@
import * as React from 'react'; import * as React from 'react';
import { times } from 'lodash'; import { times } from 'lodash';
import { v4 as generateUuid } from 'uuid';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean, select, number } from '@storybook/addon-knobs'; import { boolean, select, number } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
@ -21,7 +20,10 @@ import type { PropsType } from './CallScreen';
import { CallScreen } from './CallScreen'; import { CallScreen } from './CallScreen';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; import {
getDefaultConversation,
getDefaultConversationWithUuid,
} from '../test-both/helpers/getDefaultConversation';
import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource'; import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
@ -286,10 +288,9 @@ const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
presenting: false, presenting: false,
sharingScreen: false, sharingScreen: false,
videoAspectRatio: 1.3, videoAspectRatio: 1.3,
...getDefaultConversation({ ...getDefaultConversationWithUuid({
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1, isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
title: `Participant ${index + 1}`, title: `Participant ${index + 1}`,
uuid: generateUuid(),
}), }),
})); }));

View file

@ -38,6 +38,7 @@ import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPermissionsModal'; import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPermissionsModal';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import type { UUIDStringType } from '../types/UUID';
import * as KeyboardLayout from '../services/keyboardLayout'; import * as KeyboardLayout from '../services/keyboardLayout';
import { useActivateSpeakerViewOnPresenting } from '../hooks/useActivateSpeakerViewOnPresenting'; import { useActivateSpeakerViewOnPresenting } from '../hooks/useActivateSpeakerViewOnPresenting';
@ -57,7 +58,7 @@ export type PropsType = {
phoneNumber?: string; phoneNumber?: string;
profileName?: string; profileName?: string;
title: string; title: string;
uuid: string; uuid: UUIDStringType;
}; };
openSystemPreferencesAction: () => unknown; openSystemPreferencesAction: () => unknown;
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void; setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;

View file

@ -6,15 +6,18 @@ import { times } from 'lodash';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean } from '@storybook/addon-knobs'; import { boolean } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { v4 as generateUuid } from 'uuid';
import { AvatarColors } from '../types/Colors'; import { AvatarColors } from '../types/Colors';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import type { PropsType } from './CallingLobby'; import type { PropsType } from './CallingLobby';
import { CallingLobby } from './CallingLobby'; import { CallingLobby } from './CallingLobby';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import { UUID } from '../types/UUID';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; import {
getDefaultConversation,
getDefaultConversationWithUuid,
} from '../test-both/helpers/getDefaultConversation';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -60,8 +63,8 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
isCallFull: boolean('isCallFull', overrideProps.isCallFull || false), isCallFull: boolean('isCallFull', overrideProps.isCallFull || false),
me: overrideProps.me || { me: overrideProps.me || {
color: AvatarColors[0], color: AvatarColors[0],
id: generateUuid(), id: UUID.generate().toString(),
uuid: generateUuid(), uuid: UUID.generate().toString(),
}, },
onCallCanceled: action('on-call-canceled'), onCallCanceled: action('on-call-canceled'),
onJoinCall: action('on-join-call'), onJoinCall: action('on-join-call'),
@ -81,8 +84,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
}; };
const fakePeekedParticipant = (conversationProps: Partial<ConversationType>) => const fakePeekedParticipant = (conversationProps: Partial<ConversationType>) =>
getDefaultConversation({ getDefaultConversationWithUuid({
uuid: generateUuid(),
...conversationProps, ...conversationProps,
}); });
@ -106,8 +108,8 @@ story.add('No Camera, local avatar', () => {
me: { me: {
avatarPath: '/fixtures/kitten-4-112-112.jpg', avatarPath: '/fixtures/kitten-4-112-112.jpg',
color: AvatarColors[0], color: AvatarColors[0],
id: generateUuid(), id: UUID.generate().toString(),
uuid: generateUuid(), uuid: UUID.generate().toString(),
}, },
}); });
return <CallingLobby {...props} />; return <CallingLobby {...props} />;
@ -141,11 +143,11 @@ story.add('Group Call - 1 peeked participant', () => {
}); });
story.add('Group Call - 1 peeked participant (self)', () => { story.add('Group Call - 1 peeked participant (self)', () => {
const uuid = generateUuid(); const uuid = UUID.generate().toString();
const props = createProps({ const props = createProps({
isGroupCall: true, isGroupCall: true,
me: { me: {
id: generateUuid(), id: UUID.generate().toString(),
uuid, uuid,
}, },
peekedParticipants: [fakePeekedParticipant({ title: 'Ash', uuid })], peekedParticipants: [fakePeekedParticipant({ title: 'Ash', uuid })],

View file

@ -19,6 +19,7 @@ import {
} from './CallingLobbyJoinButton'; } from './CallingLobbyJoinButton';
import type { AvatarColorType } from '../types/Colors'; import type { AvatarColorType } from '../types/Colors';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import type { UUIDStringType } from '../types/UUID';
import { useIsOnline } from '../hooks/useIsOnline'; import { useIsOnline } from '../hooks/useIsOnline';
import * as KeyboardLayout from '../services/keyboardLayout'; import * as KeyboardLayout from '../services/keyboardLayout';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
@ -52,7 +53,7 @@ export type PropsType = {
avatarPath?: string; avatarPath?: string;
id: string; id: string;
color?: AvatarColorType; color?: AvatarColorType;
uuid: string; uuid: UUIDStringType;
}; };
onCallCanceled: () => void; onCallCanceled: () => void;
onJoinCall: () => void; onJoinCall: () => void;

View file

@ -5,13 +5,12 @@ import * as React from 'react';
import { sample } from 'lodash'; import { sample } from 'lodash';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { v4 as generateUuid } from 'uuid';
import type { PropsType } from './CallingParticipantsList'; import type { PropsType } from './CallingParticipantsList';
import { CallingParticipantsList } from './CallingParticipantsList'; import { CallingParticipantsList } from './CallingParticipantsList';
import { AvatarColors } from '../types/Colors'; import { AvatarColors } from '../types/Colors';
import type { GroupCallRemoteParticipantType } from '../types/Calling'; import type { GroupCallRemoteParticipantType } from '../types/Calling';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; import { getDefaultConversationWithUuid } from '../test-both/helpers/getDefaultConversation';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
@ -27,14 +26,13 @@ function createParticipant(
presenting: Boolean(participantProps.presenting), presenting: Boolean(participantProps.presenting),
sharingScreen: Boolean(participantProps.sharingScreen), sharingScreen: Boolean(participantProps.sharingScreen),
videoAspectRatio: 1.3, videoAspectRatio: 1.3,
...getDefaultConversation({ ...getDefaultConversationWithUuid({
avatarPath: participantProps.avatarPath, avatarPath: participantProps.avatarPath,
color: sample(AvatarColors), color: sample(AvatarColors),
isBlocked: Boolean(participantProps.isBlocked), isBlocked: Boolean(participantProps.isBlocked),
name: participantProps.name, name: participantProps.name,
profileName: participantProps.title, profileName: participantProps.title,
title: String(participantProps.title), title: String(participantProps.title),
uuid: generateUuid(),
}), }),
}; };
} }

View file

@ -16,6 +16,7 @@ import type { EmojiPickDataType } from './emoji/EmojiPicker';
import { convertShortName } from './emoji/lib'; import { convertShortName } from './emoji/lib';
import type { LocalizerType, BodyRangeType } from '../types/Util'; import type { LocalizerType, BodyRangeType } from '../types/Util';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import { isValidUuid } from '../types/UUID';
import { MentionBlot } from '../quill/mentions/blot'; import { MentionBlot } from '../quill/mentions/blot';
import { import {
matchEmojiImage, matchEmojiImage,
@ -465,7 +466,7 @@ export function CompositionInput(props: Props): React.ReactElement {
const currentMemberUuids = currentMembers const currentMemberUuids = currentMembers
.map(m => m.uuid) .map(m => m.uuid)
.filter((uuid): uuid is string => uuid !== undefined); .filter(isValidUuid);
const newDelta = getDeltaToRemoveStaleMentions(ops, currentMemberUuids); const newDelta = getDeltaToRemoveStaleMentions(ops, currentMemberUuids);

View file

@ -4,13 +4,12 @@
import type { FC } from 'react'; import type { FC } from 'react';
import React from 'react'; import React from 'react';
import { memoize, times } from 'lodash'; import { memoize, times } from 'lodash';
import { v4 as generateUuid } from 'uuid';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { number } from '@storybook/addon-knobs'; import { number } from '@storybook/addon-knobs';
import { GroupCallOverflowArea } from './GroupCallOverflowArea'; import { GroupCallOverflowArea } from './GroupCallOverflowArea';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; import { getDefaultConversationWithUuid } from '../test-both/helpers/getDefaultConversation';
import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource'; import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource';
import { FRAME_BUFFER_SIZE } from '../calling/constants'; import { FRAME_BUFFER_SIZE } from '../calling/constants';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
@ -26,10 +25,9 @@ const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
presenting: false, presenting: false,
sharingScreen: false, sharingScreen: false,
videoAspectRatio: 1.3, videoAspectRatio: 1.3,
...getDefaultConversation({ ...getDefaultConversationWithUuid({
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1, isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
title: `Participant ${index + 1}`, title: `Participant ${index + 1}`,
uuid: generateUuid(),
}), }),
})); }));

View file

@ -6,6 +6,7 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { setupI18n } from '../../util/setupI18n'; import { setupI18n } from '../../util/setupI18n';
import { UUID } from '../../types/UUID';
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import type { GroupV2ChangeType } from '../../groups'; import type { GroupV2ChangeType } from '../../groups';
import { SignalService as Proto } from '../../protobuf'; import { SignalService as Proto } from '../../protobuf';
@ -14,12 +15,12 @@ import { GroupV2Change } from './GroupV2Change';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const OUR_ID = 'OUR_ID'; const OUR_ID = UUID.generate().toString();
const CONTACT_A = 'CONTACT_A'; const CONTACT_A = UUID.generate().toString();
const CONTACT_B = 'CONTACT_B'; const CONTACT_B = UUID.generate().toString();
const CONTACT_C = 'CONTACT_C'; const CONTACT_C = UUID.generate().toString();
const ADMIN_A = 'ADMIN_A'; const ADMIN_A = UUID.generate().toString();
const INVITEE_A = 'INVITEE_A'; const INVITEE_A = UUID.generate().toString();
const AccessControlEnum = Proto.AccessControl.AccessRequired; const AccessControlEnum = Proto.AccessControl.AccessRequired;
const RoleEnum = Proto.Member.Role; const RoleEnum = Proto.Member.Role;
@ -35,7 +36,7 @@ const renderChange = (change: GroupV2ChangeType, groupName?: string) => (
change={change} change={change}
groupName={groupName} groupName={groupName}
i18n={i18n} i18n={i18n}
ourConversationId={OUR_ID} ourUuid={OUR_ID}
renderContact={renderContact} renderContact={renderContact}
/> />
); );
@ -62,7 +63,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
}, },
{ {
type: 'member-add', type: 'member-add',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
{ {
type: 'description', type: 'description',
@ -70,7 +71,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
}, },
{ {
type: 'member-privilege', type: 'member-privilege',
conversationId: OUR_ID, uuid: OUR_ID,
newPrivilege: RoleEnum.ADMINISTRATOR, newPrivilege: RoleEnum.ADMINISTRATOR,
}, },
], ],
@ -402,7 +403,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add', type: 'member-add',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -411,7 +412,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add', type: 'member-add',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -419,7 +420,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add', type: 'member-add',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -428,7 +429,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add', type: 'member-add',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -437,7 +438,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add', type: 'member-add',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -445,7 +446,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add', type: 'member-add',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -461,7 +462,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-invite', type: 'member-add-from-invite',
conversationId: OUR_ID, uuid: OUR_ID,
inviter: CONTACT_B, inviter: CONTACT_B,
}, },
], ],
@ -470,7 +471,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-invite', type: 'member-add-from-invite',
conversationId: OUR_ID, uuid: OUR_ID,
inviter: CONTACT_A, inviter: CONTACT_A,
}, },
], ],
@ -481,7 +482,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-invite', type: 'member-add-from-invite',
conversationId: CONTACT_A, uuid: CONTACT_A,
inviter: CONTACT_B, inviter: CONTACT_B,
}, },
], ],
@ -491,7 +492,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-invite', type: 'member-add-from-invite',
conversationId: CONTACT_B, uuid: CONTACT_B,
inviter: CONTACT_C, inviter: CONTACT_C,
}, },
], ],
@ -500,7 +501,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-invite', type: 'member-add-from-invite',
conversationId: CONTACT_A, uuid: CONTACT_A,
inviter: CONTACT_B, inviter: CONTACT_B,
}, },
], ],
@ -511,7 +512,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-invite', type: 'member-add-from-invite',
conversationId: OUR_ID, uuid: OUR_ID,
inviter: CONTACT_A, inviter: CONTACT_A,
}, },
], ],
@ -521,7 +522,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-invite', type: 'member-add-from-invite',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -530,7 +531,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-invite', type: 'member-add-from-invite',
conversationId: CONTACT_A, uuid: CONTACT_A,
inviter: OUR_ID, inviter: OUR_ID,
}, },
], ],
@ -540,7 +541,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-invite', type: 'member-add-from-invite',
conversationId: CONTACT_A, uuid: CONTACT_A,
inviter: CONTACT_B, inviter: CONTACT_B,
}, },
], ],
@ -550,7 +551,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-invite', type: 'member-add-from-invite',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -565,7 +566,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-link', type: 'member-add-from-link',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -574,7 +575,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-link', type: 'member-add-from-link',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -582,7 +583,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-link', type: 'member-add-from-link',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -597,7 +598,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-admin-approval', type: 'member-add-from-admin-approval',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -605,7 +606,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-admin-approval', type: 'member-add-from-admin-approval',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -614,7 +615,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-admin-approval', type: 'member-add-from-admin-approval',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -623,7 +624,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-admin-approval', type: 'member-add-from-admin-approval',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -631,7 +632,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-add-from-admin-approval', type: 'member-add-from-admin-approval',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -646,7 +647,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-remove', type: 'member-remove',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -655,7 +656,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-remove', type: 'member-remove',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -663,7 +664,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-remove', type: 'member-remove',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -672,7 +673,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-remove', type: 'member-remove',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -681,7 +682,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-remove', type: 'member-remove',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -690,7 +691,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-remove', type: 'member-remove',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -698,7 +699,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-remove', type: 'member-remove',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -713,7 +714,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-privilege', type: 'member-privilege',
conversationId: OUR_ID, uuid: OUR_ID,
newPrivilege: RoleEnum.ADMINISTRATOR, newPrivilege: RoleEnum.ADMINISTRATOR,
}, },
], ],
@ -722,7 +723,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-privilege', type: 'member-privilege',
conversationId: OUR_ID, uuid: OUR_ID,
newPrivilege: RoleEnum.ADMINISTRATOR, newPrivilege: RoleEnum.ADMINISTRATOR,
}, },
], ],
@ -732,7 +733,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-privilege', type: 'member-privilege',
conversationId: CONTACT_A, uuid: CONTACT_A,
newPrivilege: RoleEnum.ADMINISTRATOR, newPrivilege: RoleEnum.ADMINISTRATOR,
}, },
], ],
@ -742,7 +743,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-privilege', type: 'member-privilege',
conversationId: CONTACT_A, uuid: CONTACT_A,
newPrivilege: RoleEnum.ADMINISTRATOR, newPrivilege: RoleEnum.ADMINISTRATOR,
}, },
], ],
@ -751,7 +752,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-privilege', type: 'member-privilege',
conversationId: CONTACT_A, uuid: CONTACT_A,
newPrivilege: RoleEnum.ADMINISTRATOR, newPrivilege: RoleEnum.ADMINISTRATOR,
}, },
], ],
@ -761,7 +762,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-privilege', type: 'member-privilege',
conversationId: OUR_ID, uuid: OUR_ID,
newPrivilege: RoleEnum.DEFAULT, newPrivilege: RoleEnum.DEFAULT,
}, },
], ],
@ -770,7 +771,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-privilege', type: 'member-privilege',
conversationId: OUR_ID, uuid: OUR_ID,
newPrivilege: RoleEnum.DEFAULT, newPrivilege: RoleEnum.DEFAULT,
}, },
], ],
@ -780,7 +781,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-privilege', type: 'member-privilege',
conversationId: CONTACT_A, uuid: CONTACT_A,
newPrivilege: RoleEnum.DEFAULT, newPrivilege: RoleEnum.DEFAULT,
}, },
], ],
@ -790,7 +791,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-privilege', type: 'member-privilege',
conversationId: CONTACT_A, uuid: CONTACT_A,
newPrivilege: RoleEnum.DEFAULT, newPrivilege: RoleEnum.DEFAULT,
}, },
], ],
@ -799,7 +800,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'member-privilege', type: 'member-privilege',
conversationId: CONTACT_A, uuid: CONTACT_A,
newPrivilege: RoleEnum.DEFAULT, newPrivilege: RoleEnum.DEFAULT,
}, },
], ],
@ -815,7 +816,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-add-one', type: 'pending-add-one',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -823,7 +824,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-add-one', type: 'pending-add-one',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -832,7 +833,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-add-one', type: 'pending-add-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
}, },
], ],
})} })}
@ -841,7 +842,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-add-one', type: 'pending-add-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
}, },
], ],
})} })}
@ -849,7 +850,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-add-one', type: 'pending-add-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
}, },
], ],
})} })}
@ -896,7 +897,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
inviter: OUR_ID, inviter: OUR_ID,
}, },
], ],
@ -906,7 +907,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
inviter: OUR_ID, inviter: OUR_ID,
}, },
], ],
@ -916,7 +917,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
inviter: OUR_ID, inviter: OUR_ID,
}, },
], ],
@ -925,7 +926,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
inviter: OUR_ID, inviter: OUR_ID,
}, },
], ],
@ -935,7 +936,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
}, },
], ],
})} })}
@ -944,7 +945,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
inviter: CONTACT_B, inviter: CONTACT_B,
}, },
], ],
@ -955,7 +956,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: OUR_ID, uuid: OUR_ID,
inviter: CONTACT_B, inviter: CONTACT_B,
}, },
], ],
@ -965,7 +966,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: CONTACT_B, uuid: CONTACT_B,
inviter: CONTACT_A, inviter: CONTACT_A,
}, },
], ],
@ -976,7 +977,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
inviter: CONTACT_B, inviter: CONTACT_B,
}, },
], ],
@ -986,7 +987,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
inviter: CONTACT_B, inviter: CONTACT_B,
}, },
], ],
@ -995,7 +996,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
inviter: CONTACT_B, inviter: CONTACT_B,
}, },
], ],
@ -1006,7 +1007,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
}, },
], ],
})} })}
@ -1015,7 +1016,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
}, },
], ],
})} })}
@ -1023,7 +1024,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'pending-remove-one', type: 'pending-remove-one',
conversationId: INVITEE_A, uuid: INVITEE_A,
}, },
], ],
})} })}
@ -1128,7 +1129,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'admin-approval-add-one', type: 'admin-approval-add-one',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -1136,7 +1137,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'admin-approval-add-one', type: 'admin-approval-add-one',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -1151,7 +1152,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'admin-approval-remove-one', type: 'admin-approval-remove-one',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -1159,7 +1160,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'admin-approval-remove-one', type: 'admin-approval-remove-one',
conversationId: OUR_ID, uuid: OUR_ID,
}, },
], ],
})} })}
@ -1168,7 +1169,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'admin-approval-remove-one', type: 'admin-approval-remove-one',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -1177,7 +1178,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'admin-approval-remove-one', type: 'admin-approval-remove-one',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -1186,7 +1187,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'admin-approval-remove-one', type: 'admin-approval-remove-one',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}
@ -1194,7 +1195,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [ details: [
{ {
type: 'admin-approval-remove-one', type: 'admin-approval-remove-one',
conversationId: CONTACT_A, uuid: CONTACT_A,
}, },
], ],
})} })}

View file

@ -9,6 +9,7 @@ import type { ReplacementValuesType } from '../../types/I18N';
import type { FullJSXType } from '../Intl'; import type { FullJSXType } from '../Intl';
import { Intl } from '../Intl'; import { Intl } from '../Intl';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import type { UUIDStringType } from '../../types/UUID';
import { GroupDescriptionText } from '../GroupDescriptionText'; import { GroupDescriptionText } from '../GroupDescriptionText';
import { Button, ButtonSize, ButtonVariant } from '../Button'; import { Button, ButtonSize, ButtonVariant } from '../Button';
import { SystemMessage } from './SystemMessage'; import { SystemMessage } from './SystemMessage';
@ -21,7 +22,7 @@ import { Modal } from '../Modal';
export type PropsDataType = { export type PropsDataType = {
groupName?: string; groupName?: string;
ourConversationId: string; ourUuid: UUIDStringType;
change: GroupV2ChangeType; change: GroupV2ChangeType;
}; };
@ -78,11 +79,11 @@ const changeToIconMap = new Map<string, GroupIconType>([
function getIcon( function getIcon(
detail: GroupV2ChangeDetailType, detail: GroupV2ChangeDetailType,
fromId?: string fromId?: UUIDStringType
): GroupIconType { ): GroupIconType {
const changeType = detail.type; const changeType = detail.type;
let possibleIcon = changeToIconMap.get(changeType); let possibleIcon = changeToIconMap.get(changeType);
const isSameId = fromId === get(detail, 'conversationId', null); const isSameId = fromId === get(detail, 'uuid', null);
if (isSameId) { if (isSameId) {
if (changeType === 'member-remove') { if (changeType === 'member-remove') {
possibleIcon = 'group-leave'; possibleIcon = 'group-leave';
@ -103,7 +104,7 @@ function GroupV2Detail({
}: { }: {
detail: GroupV2ChangeDetailType; detail: GroupV2ChangeDetailType;
i18n: LocalizerType; i18n: LocalizerType;
fromId?: string; fromId?: UUIDStringType;
onButtonClick: (x: string) => unknown; onButtonClick: (x: string) => unknown;
text: FullJSXType; text: FullJSXType;
}): JSX.Element { }): JSX.Element {
@ -132,7 +133,7 @@ function GroupV2Detail({
} }
export function GroupV2Change(props: PropsType): ReactElement { export function GroupV2Change(props: PropsType): ReactElement {
const { change, groupName, i18n, ourConversationId, renderContact } = props; const { change, groupName, i18n, ourUuid, renderContact } = props;
const [groupDescription, setGroupDescription] = useState< const [groupDescription, setGroupDescription] = useState<
string | undefined string | undefined
@ -142,7 +143,7 @@ export function GroupV2Change(props: PropsType): ReactElement {
<> <>
{renderChange(change, { {renderChange(change, {
i18n, i18n,
ourConversationId, ourUuid,
renderContact, renderContact,
renderString: renderStringToIntl, renderString: renderStringToIntl,
}).map((text: FullJSXType, index: number) => ( }).map((text: FullJSXType, index: number) => (

View file

@ -301,8 +301,8 @@ const actions = () => ({
), ),
checkForAccount: action('checkForAccount'), checkForAccount: action('checkForAccount'),
clearChangedMessages: action('clearChangedMessages'), clearChangedMessages: action('clearChangedMessages'),
clearInvitedConversationsForNewlyCreatedGroup: action( clearInvitedUuidsForNewlyCreatedGroup: action(
'clearInvitedConversationsForNewlyCreatedGroup' 'clearInvitedUuidsForNewlyCreatedGroup'
), ),
setLoadCountdownStart: action('setLoadCountdownStart'), setLoadCountdownStart: action('setLoadCountdownStart'),
setIsNearBottom: action('setIsNearBottom'), setIsNearBottom: action('setIsNearBottom'),

View file

@ -130,7 +130,7 @@ export type PropsActionsType = {
groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle> groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle>
) => void; ) => void;
clearChangedMessages: (conversationId: string) => unknown; clearChangedMessages: (conversationId: string) => unknown;
clearInvitedConversationsForNewlyCreatedGroup: () => void; clearInvitedUuidsForNewlyCreatedGroup: () => void;
closeContactSpoofingReview: () => void; closeContactSpoofingReview: () => void;
setLoadCountdownStart: ( setLoadCountdownStart: (
conversationId: string, conversationId: string,
@ -231,7 +231,7 @@ const getActions = createSelector(
const unsafe = pick(props, [ const unsafe = pick(props, [
'acknowledgeGroupMemberNameCollisions', 'acknowledgeGroupMemberNameCollisions',
'clearChangedMessages', 'clearChangedMessages',
'clearInvitedConversationsForNewlyCreatedGroup', 'clearInvitedUuidsForNewlyCreatedGroup',
'closeContactSpoofingReview', 'closeContactSpoofingReview',
'setLoadCountdownStart', 'setLoadCountdownStart',
'setIsNearBottom', 'setIsNearBottom',
@ -1313,7 +1313,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
const { const {
acknowledgeGroupMemberNameCollisions, acknowledgeGroupMemberNameCollisions,
areWeAdmin, areWeAdmin,
clearInvitedConversationsForNewlyCreatedGroup, clearInvitedUuidsForNewlyCreatedGroup,
closeContactSpoofingReview, closeContactSpoofingReview,
contactSpoofingReview, contactSpoofingReview,
i18n, i18n,
@ -1566,7 +1566,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
<NewlyCreatedGroupInvitedContactsDialog <NewlyCreatedGroupInvitedContactsDialog
contacts={invitedContactsForNewlyCreatedGroup} contacts={invitedContactsForNewlyCreatedGroup}
i18n={i18n} i18n={i18n}
onClose={clearInvitedConversationsForNewlyCreatedGroup} onClose={clearInvitedUuidsForNewlyCreatedGroup}
/> />
)} )}

View file

@ -7,6 +7,7 @@ import { times } from 'lodash';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { UUID } from '../../../types/UUID';
import { setupI18n } from '../../../util/setupI18n'; import { setupI18n } from '../../../util/setupI18n';
import enMessages from '../../../../_locales/en/messages.json'; import enMessages from '../../../../_locales/en/messages.json';
import type { PropsType } from './PendingInvites'; import type { PropsType } from './PendingInvites';
@ -40,11 +41,13 @@ const conversation: ConversationType = {
sharedGroupNames: [], sharedGroupNames: [],
}; };
const OUR_UUID = UUID.generate().toString();
const createProps = (): PropsType => ({ const createProps = (): PropsType => ({
approvePendingMembership: action('approvePendingMembership'), approvePendingMembership: action('approvePendingMembership'),
conversation, conversation,
i18n, i18n,
ourConversationId: 'abc123', ourUuid: OUR_UUID,
pendingApprovalMemberships: times(5, () => ({ pendingApprovalMemberships: times(5, () => ({
member: getDefaultConversation(), member: getDefaultConversation(),
})), })),
@ -52,13 +55,13 @@ const createProps = (): PropsType => ({
...times(4, () => ({ ...times(4, () => ({
member: getDefaultConversation(), member: getDefaultConversation(),
metadata: { metadata: {
addedByUserId: 'abc123', addedByUserId: OUR_UUID,
}, },
})), })),
...times(8, () => ({ ...times(8, () => ({
member: getDefaultConversation(), member: getDefaultConversation(),
metadata: { metadata: {
addedByUserId: 'def456', addedByUserId: UUID.generate().toString(),
}, },
})), })),
], ],

View file

@ -7,6 +7,7 @@ import _ from 'lodash';
import type { ConversationType } from '../../../state/ducks/conversations'; import type { ConversationType } from '../../../state/ducks/conversations';
import type { LocalizerType } from '../../../types/Util'; import type { LocalizerType } from '../../../types/Util';
import type { UUIDStringType } from '../../../types/UUID';
import { Avatar } from '../../Avatar'; import { Avatar } from '../../Avatar';
import { ConfirmationDialog } from '../../ConfirmationDialog'; import { ConfirmationDialog } from '../../ConfirmationDialog';
import { PanelSection } from './PanelSection'; import { PanelSection } from './PanelSection';
@ -16,7 +17,7 @@ import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
export type PropsType = { export type PropsType = {
readonly conversation?: ConversationType; readonly conversation?: ConversationType;
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
readonly ourConversationId?: string; readonly ourUuid?: UUIDStringType;
readonly pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>; readonly pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
readonly pendingMemberships: ReadonlyArray<GroupV2PendingMembership>; readonly pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
readonly approvePendingMembership: (conversationId: string) => void; readonly approvePendingMembership: (conversationId: string) => void;
@ -25,7 +26,7 @@ export type PropsType = {
export type GroupV2PendingMembership = { export type GroupV2PendingMembership = {
metadata: { metadata: {
addedByUserId?: string; addedByUserId?: UUIDStringType;
}; };
member: ConversationType; member: ConversationType;
}; };
@ -54,14 +55,14 @@ export const PendingInvites: React.ComponentType<PropsType> = ({
approvePendingMembership, approvePendingMembership,
conversation, conversation,
i18n, i18n,
ourConversationId, ourUuid,
pendingMemberships, pendingMemberships,
pendingApprovalMemberships, pendingApprovalMemberships,
revokePendingMemberships, revokePendingMemberships,
}) => { }) => {
if (!conversation || !ourConversationId) { if (!conversation || !ourUuid) {
throw new Error( throw new Error(
'PendingInvites rendered without a conversation or ourConversationId' 'PendingInvites rendered without a conversation or ourUuid'
); );
} }
@ -131,7 +132,7 @@ export const PendingInvites: React.ComponentType<PropsType> = ({
i18n={i18n} i18n={i18n}
members={conversation.sortedGroupMembers || []} members={conversation.sortedGroupMembers || []}
memberships={pendingMemberships} memberships={pendingMemberships}
ourConversationId={ourConversationId} ourUuid={ourUuid}
setStagedMemberships={setStagedMemberships} setStagedMemberships={setStagedMemberships}
/> />
) : null} ) : null}
@ -142,7 +143,7 @@ export const PendingInvites: React.ComponentType<PropsType> = ({
i18n={i18n} i18n={i18n}
members={conversation.sortedGroupMembers || []} members={conversation.sortedGroupMembers || []}
onClose={() => setStagedMemberships(null)} onClose={() => setStagedMemberships(null)}
ourConversationId={ourConversationId} ourUuid={ourUuid}
revokePendingMemberships={revokePendingMemberships} revokePendingMemberships={revokePendingMemberships}
stagedMemberships={stagedMemberships} stagedMemberships={stagedMemberships}
/> />
@ -156,7 +157,7 @@ function MembershipActionConfirmation({
i18n, i18n,
members, members,
onClose, onClose,
ourConversationId, ourUuid,
revokePendingMemberships, revokePendingMemberships,
stagedMemberships, stagedMemberships,
}: { }: {
@ -164,7 +165,7 @@ function MembershipActionConfirmation({
i18n: LocalizerType; i18n: LocalizerType;
members: Array<ConversationType>; members: Array<ConversationType>;
onClose: () => void; onClose: () => void;
ourConversationId: string; ourUuid: string;
revokePendingMemberships: (conversationIds: Array<string>) => void; revokePendingMemberships: (conversationIds: Array<string>) => void;
stagedMemberships: Array<StagedMembershipType>; stagedMemberships: Array<StagedMembershipType>;
}) { }) {
@ -216,7 +217,7 @@ function MembershipActionConfirmation({
{getConfirmationMessage({ {getConfirmationMessage({
i18n, i18n,
members, members,
ourConversationId, ourUuid,
stagedMemberships, stagedMemberships,
})} })}
</ConfirmationDialog> </ConfirmationDialog>
@ -226,12 +227,12 @@ function MembershipActionConfirmation({
function getConfirmationMessage({ function getConfirmationMessage({
i18n, i18n,
members, members,
ourConversationId, ourUuid,
stagedMemberships, stagedMemberships,
}: Readonly<{ }: Readonly<{
i18n: LocalizerType; i18n: LocalizerType;
members: ReadonlyArray<ConversationType>; members: ReadonlyArray<ConversationType>;
ourConversationId: string; ourUuid: string;
stagedMemberships: ReadonlyArray<StagedMembershipType>; stagedMemberships: ReadonlyArray<StagedMembershipType>;
}>): string { }>): string {
if (!stagedMemberships || !stagedMemberships.length) { if (!stagedMemberships || !stagedMemberships.length) {
@ -261,8 +262,7 @@ function getConfirmationMessage({
const firstPendingMembership = firstMembership as GroupV2PendingMembership; const firstPendingMembership = firstMembership as GroupV2PendingMembership;
// Pending invite // Pending invite
const invitedByUs = const invitedByUs = firstPendingMembership.metadata.addedByUserId === ourUuid;
firstPendingMembership.metadata.addedByUserId === ourConversationId;
if (invitedByUs) { if (invitedByUs) {
return i18n('PendingInvites--revoke-for', { return i18n('PendingInvites--revoke-for', {
@ -364,14 +364,14 @@ function MembersPendingProfileKey({
i18n, i18n,
members, members,
memberships, memberships,
ourConversationId, ourUuid,
setStagedMemberships, setStagedMemberships,
}: Readonly<{ }: Readonly<{
conversation: ConversationType; conversation: ConversationType;
i18n: LocalizerType; i18n: LocalizerType;
members: Array<ConversationType>; members: Array<ConversationType>;
memberships: ReadonlyArray<GroupV2PendingMembership>; memberships: ReadonlyArray<GroupV2PendingMembership>;
ourConversationId: string; ourUuid: string;
setStagedMemberships: (stagedMembership: Array<StagedMembershipType>) => void; setStagedMemberships: (stagedMembership: Array<StagedMembershipType>) => void;
}>) { }>) {
const groupedPendingMemberships = _.groupBy( const groupedPendingMemberships = _.groupBy(
@ -380,7 +380,7 @@ function MembersPendingProfileKey({
); );
const { const {
[ourConversationId]: ourPendingMemberships, [ourUuid]: ourPendingMemberships,
...otherPendingMembershipGroups ...otherPendingMembershipGroups
} = groupedPendingMemberships; } = groupedPendingMemberships;

View file

@ -4,13 +4,14 @@
import type { FullJSXType } from './components/Intl'; import type { FullJSXType } from './components/Intl';
import type { LocalizerType } from './types/Util'; import type { LocalizerType } from './types/Util';
import type { ReplacementValuesType } from './types/I18N'; import type { ReplacementValuesType } from './types/I18N';
import type { UUIDStringType } from './types/UUID';
import { missingCaseError } from './util/missingCaseError'; import { missingCaseError } from './util/missingCaseError';
import type { GroupV2ChangeDetailType, GroupV2ChangeType } from './groups'; import type { GroupV2ChangeDetailType, GroupV2ChangeType } from './groups';
import { SignalService as Proto } from './protobuf'; import { SignalService as Proto } from './protobuf';
import * as log from './logging/log'; import * as log from './logging/log';
export type SmartContactRendererType = (conversationId: string) => FullJSXType; export type SmartContactRendererType = (uuid: UUIDStringType) => FullJSXType;
export type StringRendererType = ( export type StringRendererType = (
id: string, id: string,
i18n: LocalizerType, i18n: LocalizerType,
@ -18,9 +19,9 @@ export type StringRendererType = (
) => FullJSXType; ) => FullJSXType;
export type RenderOptionsType = { export type RenderOptionsType = {
from?: string; from?: UUIDStringType;
i18n: LocalizerType; i18n: LocalizerType;
ourConversationId: string; ourUuid: UUIDStringType;
renderContact: SmartContactRendererType; renderContact: SmartContactRendererType;
renderString: StringRendererType; renderString: StringRendererType;
}; };
@ -46,14 +47,8 @@ export function renderChangeDetail(
detail: GroupV2ChangeDetailType, detail: GroupV2ChangeDetailType,
options: RenderOptionsType options: RenderOptionsType
): FullJSXType { ): FullJSXType {
const { const { from, i18n, ourUuid, renderContact, renderString } = options;
from, const fromYou = Boolean(from && from === ourUuid);
i18n,
ourConversationId,
renderContact,
renderString,
} = options;
const fromYou = Boolean(from && from === ourConversationId);
if (detail.type === 'create') { if (detail.type === 'create') {
if (fromYou) { if (fromYou) {
@ -214,8 +209,8 @@ export function renderChangeDetail(
return ''; return '';
} }
if (detail.type === 'member-add') { if (detail.type === 'member-add') {
const { conversationId } = detail; const { uuid } = detail;
const weAreJoiner = conversationId === ourConversationId; const weAreJoiner = uuid === ourUuid;
if (weAreJoiner) { if (weAreJoiner) {
if (fromYou) { if (fromYou) {
@ -230,25 +225,25 @@ export function renderChangeDetail(
} }
if (fromYou) { if (fromYou) {
return renderString('GroupV2--member-add--other--you', i18n, [ return renderString('GroupV2--member-add--other--you', i18n, [
renderContact(conversationId), renderContact(uuid),
]); ]);
} }
if (from) { if (from) {
return renderString('GroupV2--member-add--other--other', i18n, { return renderString('GroupV2--member-add--other--other', i18n, {
adderName: renderContact(from), adderName: renderContact(from),
addeeName: renderContact(conversationId), addeeName: renderContact(uuid),
}); });
} }
return renderString('GroupV2--member-add--other--unknown', i18n, [ return renderString('GroupV2--member-add--other--unknown', i18n, [
renderContact(conversationId), renderContact(uuid),
]); ]);
} }
if (detail.type === 'member-add-from-invite') { if (detail.type === 'member-add-from-invite') {
const { conversationId, inviter } = detail; const { uuid, inviter } = detail;
const weAreJoiner = conversationId === ourConversationId; const weAreJoiner = uuid === ourUuid;
const weAreInviter = Boolean(inviter && inviter === ourConversationId); const weAreInviter = Boolean(inviter && inviter === ourUuid);
if (!from || from !== conversationId) { if (!from || from !== uuid) {
if (weAreJoiner) { if (weAreJoiner) {
// They can't be the same, no fromYou check here // They can't be the same, no fromYou check here
if (from) { if (from) {
@ -261,17 +256,17 @@ export function renderChangeDetail(
if (fromYou) { if (fromYou) {
return renderString('GroupV2--member-add--invited--you', i18n, { return renderString('GroupV2--member-add--invited--you', i18n, {
inviteeName: renderContact(conversationId), inviteeName: renderContact(uuid),
}); });
} }
if (from) { if (from) {
return renderString('GroupV2--member-add--invited--other', i18n, { return renderString('GroupV2--member-add--invited--other', i18n, {
memberName: renderContact(from), memberName: renderContact(from),
inviteeName: renderContact(conversationId), inviteeName: renderContact(uuid),
}); });
} }
return renderString('GroupV2--member-add--invited--unknown', i18n, { return renderString('GroupV2--member-add--invited--unknown', i18n, {
inviteeName: renderContact(conversationId), inviteeName: renderContact(uuid),
}); });
} }
@ -288,12 +283,12 @@ export function renderChangeDetail(
} }
if (weAreInviter) { if (weAreInviter) {
return renderString('GroupV2--member-add--from-invite--from-you', i18n, [ return renderString('GroupV2--member-add--from-invite--from-you', i18n, [
renderContact(conversationId), renderContact(uuid),
]); ]);
} }
if (inviter) { if (inviter) {
return renderString('GroupV2--member-add--from-invite--other', i18n, { return renderString('GroupV2--member-add--from-invite--other', i18n, {
inviteeName: renderContact(conversationId), inviteeName: renderContact(uuid),
inviterName: renderContact(inviter), inviterName: renderContact(inviter),
}); });
} }
@ -301,17 +296,17 @@ export function renderChangeDetail(
'GroupV2--member-add--from-invite--other-no-from', 'GroupV2--member-add--from-invite--other-no-from',
i18n, i18n,
{ {
inviteeName: renderContact(conversationId), inviteeName: renderContact(uuid),
} }
); );
} }
if (detail.type === 'member-add-from-link') { if (detail.type === 'member-add-from-link') {
const { conversationId } = detail; const { uuid } = detail;
if (fromYou && conversationId === ourConversationId) { if (fromYou && uuid === ourUuid) {
return renderString('GroupV2--member-add-from-link--you--you', i18n); return renderString('GroupV2--member-add-from-link--you--you', i18n);
} }
if (from && conversationId === from) { if (from && uuid === from) {
return renderString('GroupV2--member-add-from-link--other', i18n, [ return renderString('GroupV2--member-add-from-link--other', i18n, [
renderContact(from), renderContact(from),
]); ]);
@ -321,12 +316,12 @@ export function renderChangeDetail(
// from group change events, which always have a sender. // from group change events, which always have a sender.
log.warn('member-add-from-link change type; we have no from!'); log.warn('member-add-from-link change type; we have no from!');
return renderString('GroupV2--member-add--other--unknown', i18n, [ return renderString('GroupV2--member-add--other--unknown', i18n, [
renderContact(conversationId), renderContact(uuid),
]); ]);
} }
if (detail.type === 'member-add-from-admin-approval') { if (detail.type === 'member-add-from-admin-approval') {
const { conversationId } = detail; const { uuid } = detail;
const weAreJoiner = conversationId === ourConversationId; const weAreJoiner = uuid === ourUuid;
if (weAreJoiner) { if (weAreJoiner) {
if (from) { if (from) {
@ -352,7 +347,7 @@ export function renderChangeDetail(
return renderString( return renderString(
'GroupV2--member-add-from-admin-approval--other--you', 'GroupV2--member-add-from-admin-approval--other--you',
i18n, i18n,
[renderContact(conversationId)] [renderContact(uuid)]
); );
} }
if (from) { if (from) {
@ -361,7 +356,7 @@ export function renderChangeDetail(
i18n, i18n,
{ {
adminName: renderContact(from), adminName: renderContact(from),
joinerName: renderContact(conversationId), joinerName: renderContact(uuid),
} }
); );
} }
@ -372,12 +367,12 @@ export function renderChangeDetail(
return renderString( return renderString(
'GroupV2--member-add-from-admin-approval--other--unknown', 'GroupV2--member-add-from-admin-approval--other--unknown',
i18n, i18n,
[renderContact(conversationId)] [renderContact(uuid)]
); );
} }
if (detail.type === 'member-remove') { if (detail.type === 'member-remove') {
const { conversationId } = detail; const { uuid } = detail;
const weAreLeaver = conversationId === ourConversationId; const weAreLeaver = uuid === ourUuid;
if (weAreLeaver) { if (weAreLeaver) {
if (fromYou) { if (fromYou) {
@ -393,10 +388,10 @@ export function renderChangeDetail(
if (fromYou) { if (fromYou) {
return renderString('GroupV2--member-remove--other--you', i18n, [ return renderString('GroupV2--member-remove--other--you', i18n, [
renderContact(conversationId), renderContact(uuid),
]); ]);
} }
if (from && from === conversationId) { if (from && from === uuid) {
return renderString('GroupV2--member-remove--other--self', i18n, [ return renderString('GroupV2--member-remove--other--self', i18n, [
renderContact(from), renderContact(from),
]); ]);
@ -404,16 +399,16 @@ export function renderChangeDetail(
if (from) { if (from) {
return renderString('GroupV2--member-remove--other--other', i18n, { return renderString('GroupV2--member-remove--other--other', i18n, {
adminName: renderContact(from), adminName: renderContact(from),
memberName: renderContact(conversationId), memberName: renderContact(uuid),
}); });
} }
return renderString('GroupV2--member-remove--other--unknown', i18n, [ return renderString('GroupV2--member-remove--other--unknown', i18n, [
renderContact(conversationId), renderContact(uuid),
]); ]);
} }
if (detail.type === 'member-privilege') { if (detail.type === 'member-privilege') {
const { conversationId, newPrivilege } = detail; const { uuid, newPrivilege } = detail;
const weAreMember = conversationId === ourConversationId; const weAreMember = uuid === ourUuid;
if (newPrivilege === RoleEnum.ADMINISTRATOR) { if (newPrivilege === RoleEnum.ADMINISTRATOR) {
if (weAreMember) { if (weAreMember) {
@ -435,7 +430,7 @@ export function renderChangeDetail(
return renderString( return renderString(
'GroupV2--member-privilege--promote--other--you', 'GroupV2--member-privilege--promote--other--you',
i18n, i18n,
[renderContact(conversationId)] [renderContact(uuid)]
); );
} }
if (from) { if (from) {
@ -444,14 +439,14 @@ export function renderChangeDetail(
i18n, i18n,
{ {
adminName: renderContact(from), adminName: renderContact(from),
memberName: renderContact(conversationId), memberName: renderContact(uuid),
} }
); );
} }
return renderString( return renderString(
'GroupV2--member-privilege--promote--other--unknown', 'GroupV2--member-privilege--promote--other--unknown',
i18n, i18n,
[renderContact(conversationId)] [renderContact(uuid)]
); );
} }
if (newPrivilege === RoleEnum.DEFAULT) { if (newPrivilege === RoleEnum.DEFAULT) {
@ -473,7 +468,7 @@ export function renderChangeDetail(
return renderString( return renderString(
'GroupV2--member-privilege--demote--other--you', 'GroupV2--member-privilege--demote--other--you',
i18n, i18n,
[renderContact(conversationId)] [renderContact(uuid)]
); );
} }
if (from) { if (from) {
@ -482,14 +477,14 @@ export function renderChangeDetail(
i18n, i18n,
{ {
adminName: renderContact(from), adminName: renderContact(from),
memberName: renderContact(conversationId), memberName: renderContact(uuid),
} }
); );
} }
return renderString( return renderString(
'GroupV2--member-privilege--demote--other--unknown', 'GroupV2--member-privilege--demote--other--unknown',
i18n, i18n,
[renderContact(conversationId)] [renderContact(uuid)]
); );
} }
log.warn( log.warn(
@ -498,8 +493,8 @@ export function renderChangeDetail(
return ''; return '';
} }
if (detail.type === 'pending-add-one') { if (detail.type === 'pending-add-one') {
const { conversationId } = detail; const { uuid } = detail;
const weAreInvited = conversationId === ourConversationId; const weAreInvited = uuid === ourUuid;
if (weAreInvited) { if (weAreInvited) {
if (from) { if (from) {
return renderString('GroupV2--pending-add--one--you--other', i18n, [ return renderString('GroupV2--pending-add--one--you--other', i18n, [
@ -510,7 +505,7 @@ export function renderChangeDetail(
} }
if (fromYou) { if (fromYou) {
return renderString('GroupV2--pending-add--one--other--you', i18n, [ return renderString('GroupV2--pending-add--one--other--you', i18n, [
renderContact(conversationId), renderContact(uuid),
]); ]);
} }
if (from) { if (from) {
@ -539,23 +534,23 @@ export function renderChangeDetail(
]); ]);
} }
if (detail.type === 'pending-remove-one') { if (detail.type === 'pending-remove-one') {
const { inviter, conversationId } = detail; const { inviter, uuid } = detail;
const weAreInviter = Boolean(inviter && inviter === ourConversationId); const weAreInviter = Boolean(inviter && inviter === ourUuid);
const weAreInvited = conversationId === ourConversationId; const weAreInvited = uuid === ourUuid;
const sentByInvited = Boolean(from && from === conversationId); const sentByInvited = Boolean(from && from === uuid);
const sentByInviter = Boolean(from && inviter && from === inviter); const sentByInviter = Boolean(from && inviter && from === inviter);
if (weAreInviter) { if (weAreInviter) {
if (sentByInvited) { if (sentByInvited) {
return renderString('GroupV2--pending-remove--decline--you', i18n, [ return renderString('GroupV2--pending-remove--decline--you', i18n, [
renderContact(conversationId), renderContact(uuid),
]); ]);
} }
if (fromYou) { if (fromYou) {
return renderString( return renderString(
'GroupV2--pending-remove--revoke-invite-from-you--one--you', 'GroupV2--pending-remove--revoke-invite-from-you--one--you',
i18n, i18n,
[renderContact(conversationId)] [renderContact(uuid)]
); );
} }
if (from) { if (from) {
@ -564,14 +559,14 @@ export function renderChangeDetail(
i18n, i18n,
{ {
adminName: renderContact(from), adminName: renderContact(from),
inviteeName: renderContact(conversationId), inviteeName: renderContact(uuid),
} }
); );
} }
return renderString( return renderString(
'GroupV2--pending-remove--revoke-invite-from-you--one--unknown', 'GroupV2--pending-remove--revoke-invite-from-you--one--unknown',
i18n, i18n,
[renderContact(conversationId)] [renderContact(uuid)]
); );
} }
if (sentByInvited) { if (sentByInvited) {
@ -635,7 +630,7 @@ export function renderChangeDetail(
} }
if (detail.type === 'pending-remove-many') { if (detail.type === 'pending-remove-many') {
const { count, inviter } = detail; const { count, inviter } = detail;
const weAreInviter = Boolean(inviter && inviter === ourConversationId); const weAreInviter = Boolean(inviter && inviter === ourUuid);
if (weAreInviter) { if (weAreInviter) {
if (fromYou) { if (fromYou) {
@ -714,19 +709,19 @@ export function renderChangeDetail(
); );
} }
if (detail.type === 'admin-approval-add-one') { if (detail.type === 'admin-approval-add-one') {
const { conversationId } = detail; const { uuid } = detail;
const weAreJoiner = conversationId === ourConversationId; const weAreJoiner = uuid === ourUuid;
if (weAreJoiner) { if (weAreJoiner) {
return renderString('GroupV2--admin-approval-add-one--you', i18n); return renderString('GroupV2--admin-approval-add-one--you', i18n);
} }
return renderString('GroupV2--admin-approval-add-one--other', i18n, [ return renderString('GroupV2--admin-approval-add-one--other', i18n, [
renderContact(conversationId), renderContact(uuid),
]); ]);
} }
if (detail.type === 'admin-approval-remove-one') { if (detail.type === 'admin-approval-remove-one') {
const { conversationId } = detail; const { uuid } = detail;
const weAreJoiner = conversationId === ourConversationId; const weAreJoiner = uuid === ourUuid;
if (weAreJoiner) { if (weAreJoiner) {
if (fromYou) { if (fromYou) {
@ -745,14 +740,14 @@ export function renderChangeDetail(
return renderString( return renderString(
'GroupV2--admin-approval-remove-one--other--you', 'GroupV2--admin-approval-remove-one--other--you',
i18n, i18n,
[renderContact(conversationId)] [renderContact(uuid)]
); );
} }
if (from && from === conversationId) { if (from && from === uuid) {
return renderString( return renderString(
'GroupV2--admin-approval-remove-one--other--own', 'GroupV2--admin-approval-remove-one--other--own',
i18n, i18n,
[renderContact(conversationId)] [renderContact(uuid)]
); );
} }
if (from) { if (from) {
@ -761,7 +756,7 @@ export function renderChangeDetail(
i18n, i18n,
{ {
adminName: renderContact(from), adminName: renderContact(from),
joinerName: renderContact(conversationId), joinerName: renderContact(uuid),
} }
); );
} }
@ -771,7 +766,7 @@ export function renderChangeDetail(
return renderString( return renderString(
'GroupV2--admin-approval-remove-one--other--own', 'GroupV2--admin-approval-remove-one--other--own',
i18n, i18n,
[renderContact(conversationId)] [renderContact(uuid)]
); );
} }
if (detail.type === 'group-link-add') { if (detail.type === 'group-link-add') {

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,7 @@ import { isDirectConversation } from '../util/whatTypeOfConversation';
import { getOwn } from '../util/getOwn'; import { getOwn } from '../util/getOwn';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import { createWaitBatcher } from '../util/waitBatcher'; import { createWaitBatcher } from '../util/waitBatcher';
import type { UUIDStringType } from '../types/UUID';
import { import {
SendActionType, SendActionType,
SendStatus, SendStatus,
@ -34,6 +35,7 @@ export enum MessageReceiptType {
type MessageReceiptAttributesType = { type MessageReceiptAttributesType = {
messageSentAt: number; messageSentAt: number;
receiptTimestamp: number; receiptTimestamp: number;
sourceUuid: UUIDStringType;
sourceConversationId: string; sourceConversationId: string;
sourceDevice: number; sourceDevice: number;
type: MessageReceiptType; type: MessageReceiptType;
@ -57,6 +59,7 @@ const deleteSentProtoBatcher = createWaitBatcher({
async function getTargetMessage( async function getTargetMessage(
sourceId: string, sourceId: string,
sourceUuid: UUIDStringType,
messages: MessageModelCollectionType messages: MessageModelCollectionType
): Promise<MessageModel | null> { ): Promise<MessageModel | null> {
if (messages.length === 0) { if (messages.length === 0) {
@ -70,9 +73,12 @@ async function getTargetMessage(
return window.MessageController.register(message.id, message); return window.MessageController.register(message.id, message);
} }
const groups = await window.Signal.Data.getAllGroupsInvolvingId(sourceId, { const groups = await window.Signal.Data.getAllGroupsInvolvingUuid(
ConversationCollection: window.Whisper.ConversationCollection, sourceUuid,
}); {
ConversationCollection: window.Whisper.ConversationCollection,
}
);
const ids = groups.pluck('id'); const ids = groups.pluck('id');
ids.push(sourceId); ids.push(sourceId);
@ -136,6 +142,7 @@ export class MessageReceipts extends Collection<MessageReceiptModel> {
const type = receipt.get('type'); const type = receipt.get('type');
const messageSentAt = receipt.get('messageSentAt'); const messageSentAt = receipt.get('messageSentAt');
const sourceConversationId = receipt.get('sourceConversationId'); const sourceConversationId = receipt.get('sourceConversationId');
const sourceUuid = receipt.get('sourceUuid');
try { try {
const messages = await window.Signal.Data.getMessagesBySentAt( const messages = await window.Signal.Data.getMessagesBySentAt(
@ -145,7 +152,11 @@ export class MessageReceipts extends Collection<MessageReceiptModel> {
} }
); );
const message = await getTargetMessage(sourceConversationId, messages); const message = await getTargetMessage(
sourceConversationId,
sourceUuid,
messages
);
if (!message) { if (!message) {
log.info( log.info(
'No message for receipt', 'No message for receipt',

13
ts/model-types.d.ts vendored
View file

@ -26,6 +26,7 @@ import { AttachmentType, ThumbnailType } from './types/Attachment';
import { EmbeddedContactType } from './types/EmbeddedContact'; import { EmbeddedContactType } from './types/EmbeddedContact';
import { SignalService as Proto } from './protobuf'; import { SignalService as Proto } from './protobuf';
import { AvatarDataType } from './types/Avatar'; import { AvatarDataType } from './types/Avatar';
import { UUIDStringType } from './types/UUID';
import AccessRequiredEnum = Proto.AccessControl.AccessRequired; import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
import MemberRoleEnum = Proto.Member.Role; import MemberRoleEnum = Proto.Member.Role;
@ -181,7 +182,7 @@ export type MessageAttributesType = {
serverGuid?: string; serverGuid?: string;
serverTimestamp?: number; serverTimestamp?: number;
source?: string; source?: string;
sourceUuid?: string; sourceUuid?: UUIDStringType;
timestamp: number; timestamp: number;
@ -253,7 +254,7 @@ export type ConversationAttributesType = {
version: number; version: number;
// Private core info // Private core info
uuid?: string; uuid?: UUIDStringType;
e164?: string; e164?: string;
// Private other fields // Private other fields
@ -327,7 +328,7 @@ export type ConversationAttributesType = {
}; };
export type GroupV2MemberType = { export type GroupV2MemberType = {
conversationId: string; uuid: UUIDStringType;
role: MemberRoleEnum; role: MemberRoleEnum;
joinedAtVersion: number; joinedAtVersion: number;
@ -339,14 +340,14 @@ export type GroupV2MemberType = {
}; };
export type GroupV2PendingMemberType = { export type GroupV2PendingMemberType = {
addedByUserId?: string; addedByUserId?: UUIDStringType;
conversationId: string; uuid: UUIDStringType;
timestamp: number; timestamp: number;
role: MemberRoleEnum; role: MemberRoleEnum;
}; };
export type GroupV2PendingAdminApprovalType = { export type GroupV2PendingAdminApprovalType = {
conversationId: string; uuid: UUIDStringType;
timestamp: number; timestamp: number;
}; };

View file

@ -46,7 +46,8 @@ import { sniffImageMimeType } from '../util/sniffImageMimeType';
import { isValidE164 } from '../util/isValidE164'; import { isValidE164 } from '../util/isValidE164';
import type { MIMEType } from '../types/MIME'; import type { MIMEType } from '../types/MIME';
import { IMAGE_JPEG, IMAGE_GIF, IMAGE_WEBP } from '../types/MIME'; import { IMAGE_JPEG, IMAGE_GIF, IMAGE_WEBP } from '../types/MIME';
import { UUID } from '../types/UUID'; import { UUID, isValidUuid } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
import { deriveAccessKey, decryptProfileName, decryptProfile } from '../Crypto'; import { deriveAccessKey, decryptProfileName, decryptProfile } from '../Crypto';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import type { BodyRangesType } from '../types/Util'; import type { BodyRangesType } from '../types/Util';
@ -242,7 +243,7 @@ export class ConversationModel extends window.Backbone
initialize(attributes: Partial<ConversationAttributesType> = {}): void { initialize(attributes: Partial<ConversationAttributesType> = {}): void {
if (isValidE164(attributes.id, false)) { if (isValidE164(attributes.id, false)) {
this.set({ id: window.getGuid(), e164: attributes.id }); this.set({ id: UUID.generate().toString(), e164: attributes.id });
} }
this.storeName = 'conversations'; this.storeName = 'conversations';
@ -340,7 +341,7 @@ export class ConversationModel extends window.Backbone
} }
} }
isMemberRequestingToJoin(conversationId: string): boolean { isMemberRequestingToJoin(id: string): boolean {
if (!isGroupV2(this.attributes)) { if (!isGroupV2(this.attributes)) {
return false; return false;
} }
@ -350,12 +351,11 @@ export class ConversationModel extends window.Backbone
return false; return false;
} }
return pendingAdminApprovalV2.some( const uuid = UUID.checkedLookup(id).toString();
item => item.conversationId === conversationId return pendingAdminApprovalV2.some(item => item.uuid === uuid);
);
} }
isMemberPending(conversationId: string): boolean { isMemberPending(id: string): boolean {
if (!isGroupV2(this.attributes)) { if (!isGroupV2(this.attributes)) {
return false; return false;
} }
@ -365,13 +365,11 @@ export class ConversationModel extends window.Backbone
return false; return false;
} }
return window._.any( const uuid = UUID.checkedLookup(id).toString();
pendingMembersV2, return window._.any(pendingMembersV2, item => item.uuid === uuid);
item => item.conversationId === conversationId
);
} }
isMemberAwaitingApproval(conversationId: string): boolean { isMemberAwaitingApproval(id: string): boolean {
if (!isGroupV2(this.attributes)) { if (!isGroupV2(this.attributes)) {
return false; return false;
} }
@ -381,13 +379,11 @@ export class ConversationModel extends window.Backbone
return false; return false;
} }
return window._.any( const uuid = UUID.checkedLookup(id).toString();
pendingAdminApprovalV2, return window._.any(pendingAdminApprovalV2, item => item.uuid === uuid);
item => item.conversationId === conversationId
);
} }
isMember(conversationId: string): boolean { isMember(id: string): boolean {
if (!isGroupV2(this.attributes)) { if (!isGroupV2(this.attributes)) {
throw new Error( throw new Error(
`isMember: Called for non-GroupV2 conversation ${this.idForLogging()}` `isMember: Called for non-GroupV2 conversation ${this.idForLogging()}`
@ -398,11 +394,9 @@ export class ConversationModel extends window.Backbone
if (!membersV2 || !membersV2.length) { if (!membersV2 || !membersV2.length) {
return false; return false;
} }
const uuid = UUID.checkedLookup(id).toString();
return window._.any( return window._.any(membersV2, item => item.uuid === uuid);
membersV2,
item => item.conversationId === conversationId
);
} }
async updateExpirationTimerInGroupV2( async updateExpirationTimerInGroupV2(
@ -678,7 +672,7 @@ export class ConversationModel extends window.Backbone
} }
return uuid; return uuid;
}) })
.filter((uuid): uuid is string => Boolean(uuid)); .filter(isNotNil);
if (!uuids.length) { if (!uuids.length) {
return undefined; return undefined;
@ -1565,7 +1559,7 @@ export class ConversationModel extends window.Backbone
updateUuid(uuid?: string): void { updateUuid(uuid?: string): void {
const oldValue = this.get('uuid'); const oldValue = this.get('uuid');
if (uuid && uuid !== oldValue) { if (uuid && uuid !== oldValue) {
this.set('uuid', uuid.toLowerCase()); this.set('uuid', UUID.cast(uuid.toLowerCase()));
window.Signal.Data.updateConversation(this.attributes); window.Signal.Data.updateConversation(this.attributes);
this.trigger('idUpdated', this, 'uuid', oldValue); this.trigger('idUpdated', this, 'uuid', oldValue);
} }
@ -1840,6 +1834,7 @@ export class ConversationModel extends window.Backbone
approvalRequired: boolean; approvalRequired: boolean;
}): Promise<void> { }): Promise<void> {
const ourConversationId = window.ConversationController.getOurConversationIdOrThrow(); const ourConversationId = window.ConversationController.getOurConversationIdOrThrow();
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
try { try {
if (approvalRequired) { if (approvalRequired) {
await this.modifyGroupV2({ await this.modifyGroupV2({
@ -1870,7 +1865,7 @@ export class ConversationModel extends window.Backbone
this.set({ this.set({
pendingAdminApprovalV2: [ pendingAdminApprovalV2: [
{ {
conversationId: ourConversationId, uuid: ourUuid,
timestamp: Date.now(), timestamp: Date.now(),
}, },
], ],
@ -2143,14 +2138,12 @@ export class ConversationModel extends window.Backbone
} }
async safeGetVerified(): Promise<number> { async safeGetVerified(): Promise<number> {
const uuid = this.get('uuid'); const uuid = this.getUuid();
if (!uuid) { if (!uuid) {
return window.textsecure.storage.protocol.VerifiedStatus.DEFAULT; return window.textsecure.storage.protocol.VerifiedStatus.DEFAULT;
} }
const promise = window.textsecure.storage.protocol.getVerified( const promise = window.textsecure.storage.protocol.getVerified(uuid);
new UUID(uuid)
);
return promise.catch( return promise.catch(
() => window.textsecure.storage.protocol.VerifiedStatus.DEFAULT () => window.textsecure.storage.protocol.VerifiedStatus.DEFAULT
); );
@ -2227,7 +2220,7 @@ export class ConversationModel extends window.Backbone
); );
} }
const uuid = this.get('uuid'); const uuid = this.getUuid();
const beginningVerified = this.get('verified'); const beginningVerified = this.get('verified');
let keyChange; let keyChange;
if (options.viaSyncMessage) { if (options.viaSyncMessage) {
@ -2239,13 +2232,13 @@ export class ConversationModel extends window.Backbone
// handle the incoming key from the sync messages - need different // handle the incoming key from the sync messages - need different
// behavior if that key doesn't match the current key // behavior if that key doesn't match the current key
keyChange = await window.textsecure.storage.protocol.processVerifiedMessage( keyChange = await window.textsecure.storage.protocol.processVerifiedMessage(
new UUID(uuid), uuid,
verified, verified,
options.key || undefined options.key || undefined
); );
} else if (uuid) { } else if (uuid) {
keyChange = await window.textsecure.storage.protocol.setVerified( keyChange = await window.textsecure.storage.protocol.setVerified(
new UUID(uuid), uuid,
verified verified
); );
} else { } else {
@ -2289,10 +2282,10 @@ export class ConversationModel extends window.Backbone
async sendVerifySyncMessage( async sendVerifySyncMessage(
e164: string | undefined, e164: string | undefined,
uuid: string, uuid: UUID,
state: number state: number
): Promise<CallbackResultType | void> { ): Promise<CallbackResultType | void> {
const identifier = uuid || e164; const identifier = uuid ? uuid.toString() : e164;
if (!identifier) { if (!identifier) {
throw new Error( throw new Error(
'sendVerifySyncMessage: Neither e164 nor UUID were provided' 'sendVerifySyncMessage: Neither e164 nor UUID were provided'
@ -2328,7 +2321,7 @@ export class ConversationModel extends window.Backbone
await handleMessageSend( await handleMessageSend(
window.textsecure.messaging.syncVerification( window.textsecure.messaging.syncVerification(
e164, e164,
uuid, uuid.toString(),
state, state,
key, key,
options options
@ -2402,20 +2395,20 @@ export class ConversationModel extends window.Backbone
); );
} }
const uuid = this.get('uuid'); const uuid = this.getUuid();
if (!uuid) { if (!uuid) {
log.warn(`setApproved(${this.id}): no uuid, ignoring`); log.warn(`setApproved(${this.id}): no uuid, ignoring`);
return; return;
} }
return window.textsecure.storage.protocol.setApproval(new UUID(uuid), true); return window.textsecure.storage.protocol.setApproval(uuid, true);
} }
safeIsUntrusted(): boolean { safeIsUntrusted(): boolean {
const uuid = this.get('uuid');
try { try {
const uuid = this.getUuid();
strictAssert(uuid, `No uuid for conversation: ${this.id}`); strictAssert(uuid, `No uuid for conversation: ${this.id}`);
return window.textsecure.storage.protocol.isUntrusted(new UUID(uuid)); return window.textsecure.storage.protocol.isUntrusted(uuid);
} catch (err) { } catch (err) {
return false; return false;
} }
@ -2671,8 +2664,9 @@ export class ConversationModel extends window.Backbone
this.trigger('newmessage', model); this.trigger('newmessage', model);
if (isDirectConversation(this.attributes)) { const uuid = this.getUuid();
window.ConversationController.getAllGroupsInvolvingId(this.id).then( if (isDirectConversation(this.attributes) && uuid) {
window.ConversationController.getAllGroupsInvolvingUuid(uuid).then(
groups => { groups => {
window._.forEach(groups, group => { window._.forEach(groups, group => {
group.addVerifiedChange(this.id, verified, options); group.addVerifiedChange(this.id, verified, options);
@ -2790,8 +2784,9 @@ export class ConversationModel extends window.Backbone
this.trigger('newmessage', model); this.trigger('newmessage', model);
if (isDirectConversation(this.attributes)) { const uuid = this.getUuid();
window.ConversationController.getAllGroupsInvolvingId(this.id).then( if (isDirectConversation(this.attributes) && uuid) {
window.ConversationController.getAllGroupsInvolvingUuid(uuid).then(
groups => { groups => {
window._.forEach(groups, group => { window._.forEach(groups, group => {
group.addProfileChange(profileChange, this.id); group.addProfileChange(profileChange, this.id);
@ -2899,17 +2894,21 @@ export class ConversationModel extends window.Backbone
`Conversation ${this.idForLogging()}: adding change number notification` `Conversation ${this.idForLogging()}: adding change number notification`
); );
const sourceUuid = this.getCheckedUuid(
'Change number notification without uuid'
);
const convos = [ const convos = [
this, this,
...(await window.ConversationController.getAllGroupsInvolvingId(this.id)), ...(await window.ConversationController.getAllGroupsInvolvingUuid(
sourceUuid
)),
]; ];
const sourceUuid = this.get('uuid');
await Promise.all( await Promise.all(
convos.map(convo => { convos.map(convo => {
return convo.addNotification('change-number-notification', { return convo.addNotification('change-number-notification', {
sourceUuid, sourceUuid: sourceUuid.toString(),
}); });
}) })
); );
@ -2998,7 +2997,7 @@ export class ConversationModel extends window.Backbone
validateUuid(): string | null { validateUuid(): string | null {
if (isDirectConversation(this.attributes) && this.get('uuid')) { if (isDirectConversation(this.attributes) && this.get('uuid')) {
if (window.isValidGuid(this.get('uuid'))) { if (isValidUuid(this.get('uuid'))) {
return null; return null;
} }
@ -3037,13 +3036,14 @@ export class ConversationModel extends window.Backbone
}); });
} }
isAdmin(conversationId: string): boolean { isAdmin(id: string): boolean {
if (!isGroupV2(this.attributes)) { if (!isGroupV2(this.attributes)) {
return false; return false;
} }
const uuid = UUID.checkedLookup(id).toString();
const members = this.get('membersV2') || []; const members = this.get('membersV2') || [];
const member = members.find(x => x.conversationId === conversationId); const member = members.find(x => x.uuid === uuid);
if (!member) { if (!member) {
return false; return false;
} }
@ -3053,8 +3053,19 @@ export class ConversationModel extends window.Backbone
return member.role === MEMBER_ROLES.ADMINISTRATOR; return member.role === MEMBER_ROLES.ADMINISTRATOR;
} }
getUuid(): UUID | undefined {
const value = this.get('uuid');
return value && new UUID(value);
}
getCheckedUuid(reason: string): UUID {
const result = this.getUuid();
strictAssert(result !== undefined, reason);
return result;
}
private getMemberships(): Array<{ private getMemberships(): Array<{
conversationId: string; uuid: UUIDStringType;
isAdmin: boolean; isAdmin: boolean;
}> { }> {
if (!isGroupV2(this.attributes)) { if (!isGroupV2(this.attributes)) {
@ -3064,7 +3075,7 @@ export class ConversationModel extends window.Backbone
const members = this.get('membersV2') || []; const members = this.get('membersV2') || [];
return members.map(member => ({ return members.map(member => ({
isAdmin: member.role === Proto.Member.Role.ADMINISTRATOR, isAdmin: member.role === Proto.Member.Role.ADMINISTRATOR,
conversationId: member.conversationId, uuid: member.uuid,
})); }));
} }
@ -3081,8 +3092,8 @@ export class ConversationModel extends window.Backbone
} }
private getPendingMemberships(): Array<{ private getPendingMemberships(): Array<{
addedByUserId?: string; addedByUserId?: UUIDStringType;
conversationId: string; uuid: UUIDStringType;
}> { }> {
if (!isGroupV2(this.attributes)) { if (!isGroupV2(this.attributes)) {
return []; return [];
@ -3091,18 +3102,18 @@ export class ConversationModel extends window.Backbone
const members = this.get('pendingMembersV2') || []; const members = this.get('pendingMembersV2') || [];
return members.map(member => ({ return members.map(member => ({
addedByUserId: member.addedByUserId, addedByUserId: member.addedByUserId,
conversationId: member.conversationId, uuid: member.uuid,
})); }));
} }
private getPendingApprovalMemberships(): Array<{ conversationId: string }> { private getPendingApprovalMemberships(): Array<{ uuid: UUIDStringType }> {
if (!isGroupV2(this.attributes)) { if (!isGroupV2(this.attributes)) {
return []; return [];
} }
const members = this.get('pendingAdminApprovalV2') || []; const members = this.get('pendingAdminApprovalV2') || [];
return members.map(member => ({ return members.map(member => ({
conversationId: member.conversationId, uuid: member.uuid,
})); }));
} }
@ -3136,6 +3147,13 @@ export class ConversationModel extends window.Backbone
return members.map(member => member.id); return members.map(member => member.id);
} }
getMemberUuids(): Array<UUID> {
const members = this.getMembers();
return members.map(member => {
return member.getCheckedUuid('Group member without uuid');
});
}
getRecipients({ getRecipients({
includePendingMembers, includePendingMembers,
extraConversationsForSend, extraConversationsForSend,
@ -3356,7 +3374,7 @@ export class ConversationModel extends window.Backbone
// We are only creating this model so we can use its sync message // We are only creating this model so we can use its sync message
// sending functionality. It will not be saved to the database. // sending functionality. It will not be saved to the database.
const message = new window.Whisper.Message({ const message = new window.Whisper.Message({
id: window.getGuid(), id: UUID.generate().toString(),
type: 'outgoing', type: 'outgoing',
conversationId: this.get('id'), conversationId: this.get('id'),
sent_at: timestamp, sent_at: timestamp,
@ -3489,7 +3507,7 @@ export class ConversationModel extends window.Backbone
// We are only creating this model so we can use its sync message // We are only creating this model so we can use its sync message
// sending functionality. It will not be saved to the database. // sending functionality. It will not be saved to the database.
const message = new window.Whisper.Message({ const message = new window.Whisper.Message({
id: window.getGuid(), id: UUID.generate.toString(),
type: 'outgoing', type: 'outgoing',
conversationId: this.get('id'), conversationId: this.get('id'),
sent_at: timestamp, sent_at: timestamp,
@ -3731,7 +3749,7 @@ export class ConversationModel extends window.Backbone
} }
const attributes: MessageAttributesType = { const attributes: MessageAttributesType = {
...messageWithSchema, ...messageWithSchema,
id: window.getGuid(), id: UUID.generate().toString(),
}; };
const model = new window.Whisper.Message(attributes); const model = new window.Whisper.Message(attributes);
@ -3840,9 +3858,10 @@ export class ConversationModel extends window.Backbone
const conversationId = this.id; const conversationId = this.id;
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
const lastMessages = await window.Signal.Data.getLastConversationMessages({ const lastMessages = await window.Signal.Data.getLastConversationMessages({
conversationId, conversationId,
ourConversationId, ourUuid,
Message: window.Whisper.Message, Message: window.Whisper.Message,
}); });
@ -4361,12 +4380,16 @@ export class ConversationModel extends window.Backbone
return; return;
} }
const ourGroups = await window.ConversationController.getAllGroupsInvolvingId( const ourUuid = window.textsecure.storage.user.getCheckedUuid();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion const ourGroups = await window.ConversationController.getAllGroupsInvolvingUuid(
window.ConversationController.getOurConversationId()! ourUuid
); );
const theirGroups = await window.ConversationController.getAllGroupsInvolvingId( const theirUuid = this.getUuid();
this.id if (!theirUuid) {
return;
}
const theirGroups = await window.ConversationController.getAllGroupsInvolvingUuid(
theirUuid
); );
const sharedGroups = window._.intersection(ourGroups, theirGroups); const sharedGroups = window._.intersection(ourGroups, theirGroups);
@ -4734,8 +4757,8 @@ export class ConversationModel extends window.Backbone
const memberEnum = Proto.Member.Role; const memberEnum = Proto.Member.Role;
const members = this.get('membersV2') || []; const members = this.get('membersV2') || [];
const myId = window.ConversationController.getOurConversationId(); const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
const me = members.find(item => item.conversationId === myId); const me = members.find(item => item.uuid === ourUuid);
if (!me) { if (!me) {
return false; return false;
} }

View file

@ -33,6 +33,8 @@ import { SendMessageProtoError } from '../textsecure/Errors';
import * as expirationTimer from '../util/expirationTimer'; import * as expirationTimer from '../util/expirationTimer';
import type { ReactionType } from '../types/Reactions'; import type { ReactionType } from '../types/Reactions';
import { UUID } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
import { import {
copyStickerToAttachments, copyStickerToAttachments,
deletePackReference, deletePackReference,
@ -181,8 +183,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
INITIAL_PROTOCOL_VERSION?: number; INITIAL_PROTOCOL_VERSION?: number;
OUR_UUID?: string;
isSelected?: boolean; isSelected?: boolean;
private pendingMarkRead?: number; private pendingMarkRead?: number;
@ -223,7 +223,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
this.CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT; this.CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT;
this.INITIAL_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.INITIAL; this.INITIAL_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.INITIAL;
this.OUR_UUID = window.textsecure.storage.user.getUuid()?.toString();
this.on('change', this.notifyRedux); this.on('change', this.notifyRedux);
} }
@ -385,7 +384,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
conversationSelector: findAndFormatContact, conversationSelector: findAndFormatContact,
ourConversationId, ourConversationId,
ourNumber: window.textsecure.storage.user.getNumber(), ourNumber: window.textsecure.storage.user.getNumber(),
ourUuid: this.OUR_UUID, ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
regionCode: window.storage.get('regionCode', 'ZZ'), regionCode: window.storage.get('regionCode', 'ZZ'),
accountSelector: (identifier?: string) => { accountSelector: (identifier?: string) => {
const state = window.reduxStore.getState(); const state = window.reduxStore.getState();
@ -1104,7 +1103,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return sourceDevice || window.textsecure.storage.user.getDeviceId(); return sourceDevice || window.textsecure.storage.user.getDeviceId();
} }
getSourceUuid(): string | undefined { getSourceUuid(): UUIDStringType | undefined {
if (isIncoming(this.attributes)) { if (isIncoming(this.attributes)) {
return this.get('sourceUuid'); return this.get('sourceUuid');
} }
@ -1114,7 +1113,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
); );
} }
return this.OUR_UUID; return window.textsecure.storage.user.getUuid()?.toString();
} }
getContactId(): string | undefined { getContactId(): string | undefined {
@ -2510,7 +2509,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return; return;
} }
const messageId = window.getGuid(); const messageId = UUID.generate().toString();
// Send delivery receipts, but only for incoming sealed sender messages // Send delivery receipts, but only for incoming sealed sender messages
// and not for messages from unaccepted conversations // and not for messages from unaccepted conversations

View file

@ -36,8 +36,6 @@ import {
import { ourProfileKeyService } from './ourProfileKey'; import { ourProfileKeyService } from './ourProfileKey';
import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation'; import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation';
import * as preferredReactionEmoji from '../reactions/preferredReactionEmoji'; import * as preferredReactionEmoji from '../reactions/preferredReactionEmoji';
import { UUID } from '../types/UUID';
import * as Errors from '../types/errors';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log'; import * as log from '../logging/log';
@ -107,9 +105,9 @@ export async function toContactRecord(
conversation: ConversationModel conversation: ConversationModel
): Promise<Proto.ContactRecord> { ): Promise<Proto.ContactRecord> {
const contactRecord = new Proto.ContactRecord(); const contactRecord = new Proto.ContactRecord();
const uuid = conversation.get('uuid'); const uuid = conversation.getUuid();
if (uuid) { if (uuid) {
contactRecord.serviceUuid = uuid; contactRecord.serviceUuid = uuid.toString();
} }
const e164 = conversation.get('e164'); const e164 = conversation.get('e164');
if (e164) { if (e164) {
@ -120,15 +118,8 @@ export async function toContactRecord(
contactRecord.profileKey = Bytes.fromBase64(String(profileKey)); contactRecord.profileKey = Bytes.fromBase64(String(profileKey));
} }
let maybeUuid: UUID | undefined; const identityKey = uuid
try { ? await window.textsecure.storage.protocol.loadIdentityKey(uuid)
maybeUuid = uuid ? new UUID(uuid) : undefined;
} catch (error) {
log.warn(`Invalid uuid in contact record: ${Errors.toLogFormat(error)}`);
}
const identityKey = maybeUuid
? await window.textsecure.storage.protocol.loadIdentityKey(maybeUuid)
: undefined; : undefined;
if (identityKey) { if (identityKey) {
contactRecord.identityKey = identityKey; contactRecord.identityKey = identityKey;

View file

@ -33,6 +33,7 @@ import { assert, strictAssert } from '../util/assert';
import { cleanDataForIpc } from './cleanDataForIpc'; import { cleanDataForIpc } from './cleanDataForIpc';
import type { ReactionType } from '../types/Reactions'; import type { ReactionType } from '../types/Reactions';
import type { ConversationColorType, CustomColorType } from '../types/Colors'; import type { ConversationColorType, CustomColorType } from '../types/Colors';
import type { UUIDStringType } from '../types/UUID';
import type { ProcessGroupCallRingRequestResult } from '../types/Calling'; import type { ProcessGroupCallRingRequestResult } from '../types/Calling';
import type { RemoveAllConfiguration } from '../types/RemoveAllConfiguration'; import type { RemoveAllConfiguration } from '../types/RemoveAllConfiguration';
import createTaskWithTimeout from '../textsecure/TaskWithTimeout'; import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
@ -204,7 +205,7 @@ const dataInterface: ClientInterface = {
getAllConversations, getAllConversations,
getAllConversationIds, getAllConversationIds,
getAllPrivateConversations, getAllPrivateConversations,
getAllGroupsInvolvingId, getAllGroupsInvolvingUuid,
searchConversations, searchConversations,
searchMessages, searchMessages,
@ -1044,15 +1045,15 @@ async function getAllPrivateConversations({
return collection; return collection;
} }
async function getAllGroupsInvolvingId( async function getAllGroupsInvolvingUuid(
id: string, uuid: UUIDStringType,
{ {
ConversationCollection, ConversationCollection,
}: { }: {
ConversationCollection: typeof ConversationModelCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
} }
) { ) {
const conversations = await channels.getAllGroupsInvolvingId(id); const conversations = await channels.getAllGroupsInvolvingUuid(uuid);
const collection = new ConversationCollection(); const collection = new ConversationCollection();
collection.add(conversations); collection.add(conversations);
@ -1325,11 +1326,11 @@ async function getNewerMessagesByConversation(
} }
async function getLastConversationMessages({ async function getLastConversationMessages({
conversationId, conversationId,
ourConversationId, ourUuid,
Message, Message,
}: { }: {
conversationId: string; conversationId: string;
ourConversationId: string; ourUuid: UUIDStringType;
Message: typeof MessageModel; Message: typeof MessageModel;
}): Promise<LastConversationMessagesType> { }): Promise<LastConversationMessagesType> {
const { const {
@ -1338,7 +1339,7 @@ async function getLastConversationMessages({
hasUserInitiatedMessages, hasUserInitiatedMessages,
} = await channels.getLastConversationMessages({ } = await channels.getLastConversationMessages({
conversationId, conversationId,
ourConversationId, ourUuid,
}); });
return { return {

View file

@ -63,7 +63,7 @@ export type EmojiType = {
export type IdentityKeyType = { export type IdentityKeyType = {
firstUse: boolean; firstUse: boolean;
id: UUIDStringType | `conversation:${UUIDStringType}`; id: UUIDStringType | `conversation:${string}`;
nonblockingApproval: boolean; nonblockingApproval: boolean;
publicKey: Uint8Array; publicKey: Uint8Array;
timestamp: number; timestamp: number;
@ -501,7 +501,9 @@ export type DataInterface = {
export type ServerInterface = DataInterface & { export type ServerInterface = DataInterface & {
getAllConversations: () => Promise<Array<ConversationType>>; getAllConversations: () => Promise<Array<ConversationType>>;
getAllGroupsInvolvingId: (id: string) => Promise<Array<ConversationType>>; getAllGroupsInvolvingUuid: (
id: UUIDStringType
) => Promise<Array<ConversationType>>;
getAllPrivateConversations: () => Promise<Array<ConversationType>>; getAllPrivateConversations: () => Promise<Array<ConversationType>>;
getConversationById: (id: string) => Promise<ConversationType | undefined>; getConversationById: (id: string) => Promise<ConversationType | undefined>;
getExpiredMessages: () => Promise<Array<MessageType>>; getExpiredMessages: () => Promise<Array<MessageType>>;
@ -528,7 +530,7 @@ export type ServerInterface = DataInterface & {
) => Promise<Array<MessageTypeUnhydrated>>; ) => Promise<Array<MessageTypeUnhydrated>>;
getLastConversationMessages: (options: { getLastConversationMessages: (options: {
conversationId: string; conversationId: string;
ourConversationId: string; ourUuid: UUIDStringType;
}) => Promise<LastConversationMessagesServerType>; }) => Promise<LastConversationMessagesServerType>;
getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>; getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>;
removeConversation: (id: Array<string> | string) => Promise<void>; removeConversation: (id: Array<string> | string) => Promise<void>;
@ -576,8 +578,8 @@ export type ClientInterface = DataInterface & {
getAllConversations: (options: { getAllConversations: (options: {
ConversationCollection: typeof ConversationModelCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
}) => Promise<ConversationModelCollectionType>; }) => Promise<ConversationModelCollectionType>;
getAllGroupsInvolvingId: ( getAllGroupsInvolvingUuid: (
id: string, id: UUIDStringType,
options: { options: {
ConversationCollection: typeof ConversationModelCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
} }
@ -630,7 +632,7 @@ export type ClientInterface = DataInterface & {
) => Promise<MessageModelCollectionType>; ) => Promise<MessageModelCollectionType>;
getLastConversationMessages: (options: { getLastConversationMessages: (options: {
conversationId: string; conversationId: string;
ourConversationId: string; ourUuid: UUIDStringType;
Message: typeof MessageModel; Message: typeof MessageModel;
}) => Promise<LastConversationMessagesType>; }) => Promise<LastConversationMessagesType>;
getTapToViewMessagesNeedingErase: (options: { getTapToViewMessagesNeedingErase: (options: {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,448 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from 'better-sqlite3';
import type { LoggerType } from '../../types/Logging';
import { isValidUuid } from '../../types/UUID';
import { assertSync } from '../../util/assert';
import Helpers from '../../textsecure/Helpers';
import { createOrUpdate, getById, removeById } from '../util';
import type { EmptyQuery, Query } from '../util';
import type { ItemKeyType } from '../Interface';
function getOurUuid(db: Database): string | undefined {
const UUID_ID: ItemKeyType = 'uuid_id';
const row: { json: string } | undefined = db
.prepare<Query>('SELECT json FROM items WHERE id = $id;')
.get({ id: UUID_ID });
if (!row) {
return undefined;
}
const { value } = JSON.parse(row.json);
const [ourUuid] = Helpers.unencodeNumber(String(value).toLowerCase());
return ourUuid;
}
export default function updateToSchemaVersion41(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 41) {
return;
}
const getConversationUuid = db
.prepare<Query>(
`
SELECT uuid
FROM
conversations
WHERE
id = $conversationId
`
)
.pluck();
const getConversationStats = db.prepare<Query>(
`
SELECT uuid, e164, active_at
FROM
conversations
WHERE
id = $conversationId
`
);
const compareConvoRecency = (a: string, b: string): number => {
const aStats = getConversationStats.get({ conversationId: a });
const bStats = getConversationStats.get({ conversationId: b });
const isAComplete = Boolean(aStats?.uuid && aStats?.e164);
const isBComplete = Boolean(bStats?.uuid && bStats?.e164);
if (!isAComplete && !isBComplete) {
return 0;
}
if (!isAComplete) {
return -1;
}
if (!isBComplete) {
return 1;
}
return aStats.active_at - bStats.active_at;
};
const clearSessionsAndKeys = () => {
// ts/background.ts will ask user to relink so all that matters here is
// to maintain an invariant:
//
// After this migration all sessions and keys are prefixed by
// "uuid:".
db.exec(
`
DELETE FROM senderKeys;
DELETE FROM sessions;
DELETE FROM signedPreKeys;
DELETE FROM preKeys;
`
);
assertSync(removeById<string>(db, 'items', 'identityKey'));
assertSync(removeById<string>(db, 'items', 'registrationId'));
};
const moveIdentityKeyToMap = (ourUuid: string) => {
type IdentityKeyType = {
privKey: string;
publicKey: string;
};
const identityKey = assertSync(
getById<string, { value: IdentityKeyType }>(db, 'items', 'identityKey')
);
type RegistrationId = number;
const registrationId = assertSync(
getById<string, { value: RegistrationId }>(db, 'items', 'registrationId')
);
if (identityKey) {
assertSync(
createOrUpdate<ItemKeyType>(db, 'items', {
id: 'identityKeyMap',
value: {
[ourUuid]: identityKey.value,
},
})
);
}
if (registrationId) {
assertSync(
createOrUpdate<ItemKeyType>(db, 'items', {
id: 'registrationIdMap',
value: {
[ourUuid]: registrationId.value,
},
})
);
}
db.exec(
`
DELETE FROM items WHERE id = "identityKey" OR id = "registrationId";
`
);
};
const prefixKeys = (ourUuid: string) => {
for (const table of ['signedPreKeys', 'preKeys']) {
// Update id to include suffix, add `ourUuid` and `keyId` fields.
db.prepare<Query>(
`
UPDATE ${table}
SET
id = $ourUuid || ':' || id,
json = json_set(
json,
'$.id',
$ourUuid || ':' || json_extract(json, '$.id'),
'$.keyId',
json_extract(json, '$.id'),
'$.ourUuid',
$ourUuid
)
`
).run({ ourUuid });
}
};
const updateSenderKeys = (ourUuid: string) => {
const senderKeys: ReadonlyArray<{
id: string;
senderId: string;
lastUpdatedDate: number;
}> = db
.prepare<EmptyQuery>(
'SELECT id, senderId, lastUpdatedDate FROM senderKeys'
)
.all();
logger.info(`Updating ${senderKeys.length} sender keys`);
const updateSenderKey = db.prepare<Query>(
`
UPDATE senderKeys
SET
id = $newId,
senderId = $newSenderId
WHERE
id = $id
`
);
const deleteSenderKey = db.prepare<Query>(
'DELETE FROM senderKeys WHERE id = $id'
);
const pastKeys = new Map<
string,
{
conversationId: string;
lastUpdatedDate: number;
}
>();
let updated = 0;
let deleted = 0;
let skipped = 0;
for (const { id, senderId, lastUpdatedDate } of senderKeys) {
const [conversationId] = Helpers.unencodeNumber(senderId);
const uuid = getConversationUuid.get({ conversationId });
if (!uuid) {
deleted += 1;
deleteSenderKey.run({ id });
continue;
}
const newId = `${ourUuid}:${id.replace(conversationId, uuid)}`;
const existing = pastKeys.get(newId);
// We are going to delete on of the keys anyway
if (existing) {
skipped += 1;
} else {
updated += 1;
}
const isOlder =
existing &&
(lastUpdatedDate < existing.lastUpdatedDate ||
compareConvoRecency(conversationId, existing.conversationId) < 0);
if (isOlder) {
deleteSenderKey.run({ id });
continue;
} else if (existing) {
deleteSenderKey.run({ id: newId });
}
pastKeys.set(newId, { conversationId, lastUpdatedDate });
updateSenderKey.run({
id,
newId,
newSenderId: `${senderId.replace(conversationId, uuid)}`,
});
}
logger.info(
`Updated ${senderKeys.length} sender keys: ` +
`updated: ${updated}, deleted: ${deleted}, skipped: ${skipped}`
);
};
const updateSessions = (ourUuid: string) => {
// Use uuid instead of conversation id in existing sesions and prefix id
// with ourUuid.
//
// Set ourUuid column and field in json
const allSessions = db
.prepare<EmptyQuery>('SELECT id, conversationId FROM SESSIONS')
.all();
logger.info(`Updating ${allSessions.length} sessions`);
const updateSession = db.prepare<Query>(
`
UPDATE sessions
SET
id = $newId,
ourUuid = $ourUuid,
uuid = $uuid,
json = json_set(
sessions.json,
'$.id',
$newId,
'$.uuid',
$uuid,
'$.ourUuid',
$ourUuid
)
WHERE
id = $id
`
);
const deleteSession = db.prepare<Query>(
'DELETE FROM sessions WHERE id = $id'
);
const pastSessions = new Map<
string,
{
conversationId: string;
}
>();
let updated = 0;
let deleted = 0;
let skipped = 0;
for (const { id, conversationId } of allSessions) {
const uuid = getConversationUuid.get({ conversationId });
if (!uuid) {
deleted += 1;
deleteSession.run({ id });
continue;
}
const newId = `${ourUuid}:${id.replace(conversationId, uuid)}`;
const existing = pastSessions.get(newId);
// We are going to delete on of the keys anyway
if (existing) {
skipped += 1;
} else {
updated += 1;
}
const isOlder =
existing &&
compareConvoRecency(conversationId, existing.conversationId) < 0;
if (isOlder) {
deleteSession.run({ id });
continue;
} else if (existing) {
deleteSession.run({ id: newId });
}
pastSessions.set(newId, { conversationId });
updateSession.run({
id,
newId,
uuid,
ourUuid,
});
}
logger.info(
`Updated ${allSessions.length} sessions: ` +
`updated: ${updated}, deleted: ${deleted}, skipped: ${skipped}`
);
};
const updateIdentityKeys = () => {
const identityKeys: ReadonlyArray<{
id: string;
}> = db.prepare<EmptyQuery>('SELECT id FROM identityKeys').all();
logger.info(`Updating ${identityKeys.length} identity keys`);
const updateIdentityKey = db.prepare<Query>(
`
UPDATE identityKeys
SET
id = $newId,
json = json_set(
identityKeys.json,
'$.id',
$newId
)
WHERE
id = $id
`
);
let migrated = 0;
for (const { id } of identityKeys) {
const uuid = getConversationUuid.get({ conversationId: id });
let newId: string;
if (uuid) {
migrated += 1;
newId = uuid;
} else {
newId = `conversation:${id}`;
}
updateIdentityKey.run({ id, newId });
}
logger.info(`Migrated ${migrated} identity keys`);
};
db.transaction(() => {
db.exec(
`
-- Change type of 'id' column from INTEGER to STRING
ALTER TABLE preKeys
RENAME TO old_preKeys;
ALTER TABLE signedPreKeys
RENAME TO old_signedPreKeys;
CREATE TABLE preKeys(
id STRING PRIMARY KEY ASC,
json TEXT
);
CREATE TABLE signedPreKeys(
id STRING PRIMARY KEY ASC,
json TEXT
);
-- sqlite handles the type conversion
INSERT INTO preKeys SELECT * FROM old_preKeys;
INSERT INTO signedPreKeys SELECT * FROM old_signedPreKeys;
DROP TABLE old_preKeys;
DROP TABLE old_signedPreKeys;
-- Alter sessions
ALTER TABLE sessions
ADD COLUMN ourUuid STRING;
ALTER TABLE sessions
ADD COLUMN uuid STRING;
`
);
const ourUuid = getOurUuid(db);
if (!isValidUuid(ourUuid)) {
logger.error(
'updateToSchemaVersion41: no uuid is available clearing sessions'
);
clearSessionsAndKeys();
db.pragma('user_version = 41');
return;
}
prefixKeys(ourUuid);
updateSenderKeys(ourUuid);
updateSessions(ourUuid);
moveIdentityKeyToMap(ourUuid);
updateIdentityKeys();
db.pragma('user_version = 41');
})();
logger.info('updateToSchemaVersion41: success!');
}

View file

@ -0,0 +1,77 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from 'better-sqlite3';
import { batchMultiVarQuery } from '../util';
import type { ArrayQuery } from '../util';
import type { LoggerType } from '../../types/Logging';
export default function updateToSchemaVersion42(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 42) {
return;
}
db.transaction(() => {
// First, recreate messages table delete trigger with reaction support
db.exec(`
DROP TRIGGER messages_on_delete;
CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN
DELETE FROM messages_fts WHERE rowid = old.rowid;
DELETE FROM sendLogPayloads WHERE id IN (
SELECT payloadId FROM sendLogMessageIds
WHERE messageId = old.id
);
DELETE FROM reactions WHERE rowid IN (
SELECT rowid FROM reactions
WHERE messageId = old.id
);
END;
`);
// Then, delete previously-orphaned reactions
// Note: we use `pluck` here to fetch only the first column of
// returned row.
const messageIdList: Array<string> = db
.prepare('SELECT id FROM messages ORDER BY id ASC;')
.pluck()
.all();
const allReactions: Array<{
rowid: number;
messageId: string;
}> = db.prepare('SELECT rowid, messageId FROM reactions;').all();
const messageIds = new Set(messageIdList);
const reactionsToDelete: Array<number> = [];
allReactions.forEach(reaction => {
if (!messageIds.has(reaction.messageId)) {
reactionsToDelete.push(reaction.rowid);
}
});
function deleteReactions(rowids: Array<number>) {
db.prepare<ArrayQuery>(
`
DELETE FROM reactions
WHERE rowid IN ( ${rowids.map(() => '?').join(', ')} );
`
).run(rowids);
}
if (reactionsToDelete.length > 0) {
logger.info(`Deleting ${reactionsToDelete.length} orphaned reactions`);
batchMultiVarQuery(db, reactionsToDelete, deleteReactions);
}
db.pragma('user_version = 42');
})();
logger.info('updateToSchemaVersion42: success!');
}

View file

@ -0,0 +1,417 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from 'better-sqlite3';
import { omit } from 'lodash';
import type { LoggerType } from '../../types/Logging';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import { isNotNil } from '../../util/isNotNil';
import { assert } from '../../util/assert';
import {
TableIterator,
getCountFromTable,
jsonToObject,
objectToJSON,
} from '../util';
import type { EmptyQuery, Query } from '../util';
import type { MessageType, ConversationType } from '../Interface';
export default function updateToSchemaVersion43(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 43) {
return;
}
type LegacyPendingMemberType = {
addedByUserId?: string;
conversationId: string;
};
type LegacyAdminApprovalType = {
conversationId: string;
};
type LegacyConversationType = {
id: string;
membersV2?: Array<{
conversationId: string;
}>;
pendingMembersV2?: Array<LegacyPendingMemberType>;
pendingAdminApprovalV2?: Array<LegacyAdminApprovalType>;
};
const getConversationUuid = db
.prepare<Query>(
`
SELECT uuid
FROM
conversations
WHERE
id = $conversationId
`
)
.pluck();
const updateConversationStmt = db.prepare(
`
UPDATE conversations SET
json = $json,
members = $members
WHERE id = $id;
`
);
const updateMessageStmt = db.prepare(
`
UPDATE messages SET
json = $json,
sourceUuid = $sourceUuid
WHERE id = $id;
`
);
const upgradeConversation = (convo: ConversationType) => {
const legacy = (convo as unknown) as LegacyConversationType;
let result = convo;
const memberKeys: Array<keyof LegacyConversationType> = [
'membersV2',
'pendingMembersV2',
'pendingAdminApprovalV2',
];
for (const key of memberKeys) {
const oldValue = legacy[key];
if (!Array.isArray(oldValue)) {
continue;
}
let addedByCount = 0;
const newValue = oldValue
.map(member => {
const uuid: UUIDStringType = getConversationUuid.get({
conversationId: member.conversationId,
});
if (!uuid) {
logger.warn(
`updateToSchemaVersion43: ${legacy.id}.${key} UUID not found ` +
`for ${member.conversationId}`
);
return undefined;
}
const updated = {
...omit(member, 'conversationId'),
uuid,
};
// We previously stored our conversation
if (!('addedByUserId' in member) || !member.addedByUserId) {
return updated;
}
const addedByUserId:
| UUIDStringType
| undefined = getConversationUuid.get({
conversationId: member.addedByUserId,
});
if (!addedByUserId) {
return updated;
}
addedByCount += 1;
return {
...updated,
addedByUserId,
};
})
.filter(isNotNil);
result = {
...result,
[key]: newValue,
};
if (oldValue.length !== 0) {
logger.info(
`updateToSchemaVersion43: migrated ${oldValue.length} ${key} ` +
`entries to ${newValue.length} for ${legacy.id}`
);
}
if (addedByCount > 0) {
logger.info(
`updateToSchemaVersion43: migrated ${addedByCount} addedByUserId ` +
`in ${key} for ${legacy.id}`
);
}
}
if (result === convo) {
return;
}
let dbMembers: string | null;
if (result.membersV2) {
dbMembers = result.membersV2.map(item => item.uuid).join(' ');
} else if (result.members) {
dbMembers = result.members.join(' ');
} else {
dbMembers = null;
}
updateConversationStmt.run({
id: result.id,
json: objectToJSON(result),
members: dbMembers,
});
};
type LegacyMessageType = {
id: string;
groupV2Change?: {
from: string;
details: Array<
(
| {
type:
| 'member-add'
| 'member-add-from-invite'
| 'member-add-from-link'
| 'member-add-from-admin-approval'
| 'member-privilege'
| 'member-remove'
| 'pending-add-one'
| 'pending-remove-one'
| 'admin-approval-add-one'
| 'admin-approval-remove-one';
conversationId: string;
}
| {
type: unknown;
conversationId?: undefined;
}
) &
(
| {
type:
| 'member-add-from-invite'
| 'pending-remove-one'
| 'pending-remove-many'
| 'admin-approval-remove-one';
inviter: string;
}
| {
inviter?: undefined;
}
)
>;
};
sourceUuid: string;
invitedGV2Members?: Array<LegacyPendingMemberType>;
};
const upgradeMessage = (message: MessageType): boolean => {
const {
id,
groupV2Change,
sourceUuid,
invitedGV2Members,
} = (message as unknown) as LegacyMessageType;
let result = message;
if (groupV2Change) {
assert(result.groupV2Change, 'Pacify typescript');
const from: UUIDStringType | undefined = getConversationUuid.get({
conversationId: groupV2Change.from,
});
if (from) {
result = {
...result,
groupV2Change: {
...result.groupV2Change,
from,
},
};
} else {
result = {
...result,
groupV2Change: omit(result.groupV2Change, ['from']),
};
}
let changedDetails = false;
const details = groupV2Change.details
.map((legacyDetail, i) => {
const oldDetail = result.groupV2Change?.details[i];
assert(oldDetail, 'Pacify typescript');
let newDetail = oldDetail;
for (const key of ['conversationId' as const, 'inviter' as const]) {
const oldValue = legacyDetail[key];
const newKey = key === 'conversationId' ? 'uuid' : key;
if (oldValue === undefined) {
continue;
}
changedDetails = true;
let newValue: UUIDStringType | null = getConversationUuid.get({
conversationId: oldValue,
});
if (key === 'inviter') {
newValue = newValue ?? UUID.cast(oldValue);
}
if (!newValue) {
logger.warn(
`updateToSchemaVersion43: ${id}.groupV2Change.details.${key} ` +
`UUID not found for ${oldValue}`
);
return undefined;
}
assert(newDetail.type === legacyDetail.type, 'Pacify typescript');
newDetail = {
...omit(newDetail, key),
[newKey]: newValue,
};
}
return newDetail;
})
.filter(isNotNil);
if (changedDetails) {
result = {
...result,
groupV2Change: {
...result.groupV2Change,
details,
},
};
}
}
if (sourceUuid) {
const newValue: UUIDStringType =
getConversationUuid.get({
conversationId: sourceUuid,
}) ?? UUID.cast(sourceUuid);
if (newValue !== sourceUuid) {
result = {
...result,
sourceUuid: newValue,
};
}
}
if (invitedGV2Members) {
const newMembers = invitedGV2Members
.map(({ addedByUserId, conversationId }, i) => {
const uuid: UUIDStringType | null = getConversationUuid.get({
conversationId,
});
const oldMember =
result.invitedGV2Members && result.invitedGV2Members[i];
assert(oldMember !== undefined, 'Pacify typescript');
if (!uuid) {
logger.warn(
`updateToSchemaVersion43: ${id}.invitedGV2Members UUID ` +
`not found for ${conversationId}`
);
return undefined;
}
const newMember = {
...omit(oldMember, ['conversationId']),
uuid,
};
if (!addedByUserId) {
return newMember;
}
const newAddedBy: UUIDStringType | null = getConversationUuid.get({
conversationId: addedByUserId,
});
if (!newAddedBy) {
return newMember;
}
return {
...newMember,
addedByUserId: newAddedBy,
};
})
.filter(isNotNil);
result = {
...result,
invitedGV2Members: newMembers,
};
}
if (result === message) {
return false;
}
updateMessageStmt.run({
id: result.id,
json: JSON.stringify(result),
sourceUuid: result.sourceUuid ?? null,
});
return true;
};
db.transaction(() => {
const allConversations = db
.prepare<EmptyQuery>(
`
SELECT json, profileLastFetchedAt
FROM conversations
ORDER BY id ASC;
`
)
.all()
.map(({ json }) => jsonToObject<ConversationType>(json));
logger.info(
'updateToSchemaVersion43: About to iterate through ' +
`${allConversations.length} conversations`
);
for (const convo of allConversations) {
upgradeConversation(convo);
}
const messageCount = getCountFromTable(db, 'messages');
logger.info(
'updateToSchemaVersion43: About to iterate through ' +
`${messageCount} messages`
);
let updatedCount = 0;
for (const message of new TableIterator<MessageType>(db, 'messages')) {
if (upgradeMessage(message)) {
updatedCount += 1;
}
}
logger.info(`updateToSchemaVersion43: Updated ${updatedCount} messages`);
db.pragma('user_version = 43');
})();
logger.info('updateToSchemaVersion43: success!');
}

1934
ts/sql/migrations/index.ts Normal file

File diff suppressed because it is too large Load diff

260
ts/sql/util.ts Normal file
View file

@ -0,0 +1,260 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from 'better-sqlite3';
import { isNumber, last } from 'lodash';
export type EmptyQuery = [];
export type ArrayQuery = Array<Array<null | number | bigint | string>>;
export type Query = { [key: string]: null | number | bigint | string | Buffer };
export type JSONRows = Array<{ readonly json: string }>;
export type TableType =
| 'attachment_downloads'
| 'conversations'
| 'identityKeys'
| 'items'
| 'messages'
| 'preKeys'
| 'senderKeys'
| 'sessions'
| 'signedPreKeys'
| 'stickers'
| 'unprocessed';
// This value needs to be below SQLITE_MAX_VARIABLE_NUMBER.
const MAX_VARIABLE_COUNT = 100;
export function objectToJSON<T>(data: T): string {
return JSON.stringify(data);
}
export function jsonToObject<T>(json: string): T {
return JSON.parse(json);
}
//
// Database helpers
//
export function getSQLiteVersion(db: Database): string {
const { sqlite_version: version } = db
.prepare<EmptyQuery>('select sqlite_version() AS sqlite_version')
.get();
return version;
}
export function getSchemaVersion(db: Database): number {
return db.pragma('schema_version', { simple: true });
}
export function setUserVersion(db: Database, version: number): void {
if (!isNumber(version)) {
throw new Error(`setUserVersion: version ${version} is not a number`);
}
db.pragma(`user_version = ${version}`);
}
export function getUserVersion(db: Database): number {
return db.pragma('user_version', { simple: true });
}
export function getSQLCipherVersion(db: Database): string | undefined {
return db.pragma('cipher_version', { simple: true });
}
//
// Various table helpers
//
export function batchMultiVarQuery<ValueT>(
db: Database,
values: Array<ValueT>,
query: (batch: Array<ValueT>) => void
): [];
export function batchMultiVarQuery<ValueT, ResultT>(
db: Database,
values: Array<ValueT>,
query: (batch: Array<ValueT>) => Array<ResultT>
): Array<ResultT>;
export function batchMultiVarQuery<ValueT, ResultT>(
db: Database,
values: Array<ValueT>,
query:
| ((batch: Array<ValueT>) => void)
| ((batch: Array<ValueT>) => Array<ResultT>)
): Array<ResultT> {
if (values.length > MAX_VARIABLE_COUNT) {
const result: Array<ResultT> = [];
db.transaction(() => {
for (let i = 0; i < values.length; i += MAX_VARIABLE_COUNT) {
const batch = values.slice(i, i + MAX_VARIABLE_COUNT);
const batchResult = query(batch);
if (Array.isArray(batchResult)) {
result.push(...batchResult);
}
}
})();
return result;
}
const result = query(values);
return Array.isArray(result) ? result : [];
}
export function createOrUpdate<Key extends string | number>(
db: Database,
table: TableType,
data: Record<string, unknown> & { id: Key }
): void {
const { id } = data;
if (!id) {
throw new Error('createOrUpdate: Provided data did not have a truthy id');
}
db.prepare<Query>(
`
INSERT OR REPLACE INTO ${table} (
id,
json
) values (
$id,
$json
)
`
).run({
id,
json: objectToJSON(data),
});
}
export function bulkAdd(
db: Database,
table: TableType,
array: Array<Record<string, unknown> & { id: string | number }>
): void {
db.transaction(() => {
for (const data of array) {
createOrUpdate(db, table, data);
}
})();
}
export function getById<Key extends string | number, Result = unknown>(
db: Database,
table: TableType,
id: Key
): Result | undefined {
const row = db
.prepare<Query>(
`
SELECT *
FROM ${table}
WHERE id = $id;
`
)
.get({
id,
});
if (!row) {
return undefined;
}
return jsonToObject(row.json);
}
export function removeById<Key extends string | number>(
db: Database,
table: TableType,
id: Key | Array<Key>
): void {
if (!Array.isArray(id)) {
db.prepare<Query>(
`
DELETE FROM ${table}
WHERE id = $id;
`
).run({ id });
return;
}
if (!id.length) {
throw new Error('removeById: No ids to delete!');
}
const removeByIdsSync = (ids: Array<string | number>): void => {
db.prepare<ArrayQuery>(
`
DELETE FROM ${table}
WHERE id IN ( ${id.map(() => '?').join(', ')} );
`
).run(ids);
};
batchMultiVarQuery(db, id, removeByIdsSync);
}
export function removeAllFromTable(db: Database, table: TableType): void {
db.prepare<EmptyQuery>(`DELETE FROM ${table};`).run();
}
export function getAllFromTable<T>(db: Database, table: TableType): Array<T> {
const rows: JSONRows = db
.prepare<EmptyQuery>(`SELECT json FROM ${table};`)
.all();
return rows.map(row => jsonToObject(row.json));
}
export function getCountFromTable(db: Database, table: TableType): number {
const result: null | number = db
.prepare<EmptyQuery>(`SELECT count(*) from ${table};`)
.pluck(true)
.get();
if (isNumber(result)) {
return result;
}
throw new Error(`getCountFromTable: Unable to get count from table ${table}`);
}
export class TableIterator<ObjectType extends { id: string }> {
constructor(
private readonly db: Database,
private readonly table: TableType,
private readonly pageSize = 500
) {}
*[Symbol.iterator](): Iterator<ObjectType> {
const fetchObject = this.db.prepare<Query>(
`
SELECT json FROM ${this.table}
WHERE id > $id
ORDER BY id ASC
LIMIT $pageSize;
`
);
let complete = false;
let id = '';
while (!complete) {
const rows: JSONRows = fetchObject.all({
id,
pageSize: this.pageSize,
});
const messages: Array<ObjectType> = rows.map(row =>
jsonToObject(row.json)
);
yield* messages;
const lastMessage: ObjectType | undefined = last(messages);
if (lastMessage) {
({ id } = lastMessage);
}
complete = messages.length < this.pageSize;
}
}
}

View file

@ -35,21 +35,22 @@ import { requestCameraPermissions } from '../../util/callingPermissions';
import { isGroupCallOutboundRingEnabled } from '../../util/isGroupCallOutboundRingEnabled'; import { isGroupCallOutboundRingEnabled } from '../../util/isGroupCallOutboundRingEnabled';
import { sleep } from '../../util/sleep'; import { sleep } from '../../util/sleep';
import { LatestQueue } from '../../util/LatestQueue'; import { LatestQueue } from '../../util/LatestQueue';
import type { UUIDStringType } from '../../types/UUID';
import type { ConversationChangedActionType } from './conversations'; import type { ConversationChangedActionType } from './conversations';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
// State // State
export type GroupCallPeekInfoType = { export type GroupCallPeekInfoType = {
uuids: Array<string>; uuids: Array<UUIDStringType>;
creatorUuid?: string; creatorUuid?: UUIDStringType;
eraId?: string; eraId?: string;
maxDevices: number; maxDevices: number;
deviceCount: number; deviceCount: number;
}; };
export type GroupCallParticipantInfoType = { export type GroupCallParticipantInfoType = {
uuid: string; uuid: UUIDStringType;
demuxId: number; demuxId: number;
hasRemoteAudio: boolean; hasRemoteAudio: boolean;
hasRemoteVideo: boolean; hasRemoteVideo: boolean;
@ -77,7 +78,7 @@ type GroupCallRingStateType =
} }
| { | {
ringId: bigint; ringId: bigint;
ringerUuid: string; ringerUuid: UUIDStringType;
}; };
export type GroupCallStateType = { export type GroupCallStateType = {
@ -99,7 +100,7 @@ export type ActiveCallStateType = {
pip: boolean; pip: boolean;
presentingSource?: PresentedSource; presentingSource?: PresentedSource;
presentingSourcesAvailable?: Array<PresentableSource>; presentingSourcesAvailable?: Array<PresentableSource>;
safetyNumberChangedUuids: Array<string>; safetyNumberChangedUuids: Array<UUIDStringType>;
settingsDialogOpen: boolean; settingsDialogOpen: boolean;
showNeedsScreenRecordingPermissionsWarning?: boolean; showNeedsScreenRecordingPermissionsWarning?: boolean;
showParticipantsList: boolean; showParticipantsList: boolean;
@ -153,7 +154,7 @@ type GroupCallStateChangeArgumentType = {
}; };
type GroupCallStateChangeActionPayloadType = GroupCallStateChangeArgumentType & { type GroupCallStateChangeActionPayloadType = GroupCallStateChangeArgumentType & {
ourUuid: string; ourUuid: UUIDStringType;
}; };
export type HangUpType = { export type HangUpType = {
@ -161,7 +162,7 @@ export type HangUpType = {
}; };
type KeyChangedType = { type KeyChangedType = {
uuid: string; uuid: UUIDStringType;
}; };
export type KeyChangeOkType = { export type KeyChangeOkType = {
@ -176,7 +177,7 @@ export type IncomingDirectCallType = {
type IncomingGroupCallType = { type IncomingGroupCallType = {
conversationId: string; conversationId: string;
ringId: bigint; ringId: bigint;
ringerUuid: string; ringerUuid: UUIDStringType;
}; };
type PeekNotConnectedGroupCallType = { type PeekNotConnectedGroupCallType = {
@ -262,7 +263,7 @@ export const getActiveCall = ({
// support it for direct calls. // support it for direct calls.
export const getIncomingCall = ( export const getIncomingCall = (
callsByConversation: Readonly<CallsByConversationType>, callsByConversation: Readonly<CallsByConversationType>,
ourUuid: string ourUuid: UUIDStringType
): undefined | DirectCallStateType | GroupCallStateType => ): undefined | DirectCallStateType | GroupCallStateType =>
Object.values(callsByConversation).find(call => { Object.values(callsByConversation).find(call => {
switch (call.callMode) { switch (call.callMode) {
@ -281,7 +282,7 @@ export const getIncomingCall = (
export const isAnybodyElseInGroupCall = ( export const isAnybodyElseInGroupCall = (
{ uuids }: Readonly<GroupCallPeekInfoType>, { uuids }: Readonly<GroupCallPeekInfoType>,
ourUuid: string ourUuid: UUIDStringType
): boolean => uuids.some(id => id !== ourUuid); ): boolean => uuids.some(id => id !== ourUuid);
const getGroupCallRingState = ( const getGroupCallRingState = (
@ -390,7 +391,7 @@ type IncomingGroupCallActionType = {
type KeyChangedActionType = { type KeyChangedActionType = {
type: 'calling/MARK_CALL_UNTRUSTED'; type: 'calling/MARK_CALL_UNTRUSTED';
payload: { payload: {
safetyNumberChangedUuids: Array<string>; safetyNumberChangedUuids: Array<UUIDStringType>;
}; };
}; };
@ -409,7 +410,7 @@ export type PeekNotConnectedGroupCallFulfilledActionType = {
payload: { payload: {
conversationId: string; conversationId: string;
peekInfo: GroupCallPeekInfoType; peekInfo: GroupCallPeekInfoType;
ourConversationId: string; ourUuid: UUIDStringType;
}; };
}; };
@ -895,6 +896,8 @@ function peekNotConnectedGroupCall(
return; return;
} }
const { ourUuid } = state.user;
await calling.updateCallHistoryForGroupCall(conversationId, peekInfo); await calling.updateCallHistoryForGroupCall(conversationId, peekInfo);
const formattedPeekInfo = calling.formatGroupCallPeekInfoForRedux( const formattedPeekInfo = calling.formatGroupCallPeekInfoForRedux(
@ -906,7 +909,7 @@ function peekNotConnectedGroupCall(
payload: { payload: {
conversationId, conversationId,
peekInfo: formattedPeekInfo, peekInfo: formattedPeekInfo,
ourConversationId: state.user.ourConversationId, ourUuid,
}, },
}); });
}); });
@ -1661,7 +1664,7 @@ export function reducer(
} }
if (action.type === PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED) { if (action.type === PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED) {
const { conversationId, peekInfo, ourConversationId } = action.payload; const { conversationId, peekInfo, ourUuid } = action.payload;
const existingCall: GroupCallStateType = getGroupCall( const existingCall: GroupCallStateType = getGroupCall(
conversationId, conversationId,
@ -1693,7 +1696,7 @@ export function reducer(
} }
if ( if (
!isAnybodyElseInGroupCall(peekInfo, ourConversationId) && !isAnybodyElseInGroupCall(peekInfo, ourUuid) &&
!existingCall.ringerUuid !existingCall.ringerUuid
) { ) {
return removeConversationFromState(state, conversationId); return removeConversationFromState(state, conversationId);

View file

@ -39,6 +39,7 @@ import type {
import type { BodyRangeType } from '../../types/Util'; import type { BodyRangeType } from '../../types/Util';
import { CallMode } from '../../types/Calling'; import { CallMode } from '../../types/Calling';
import type { MediaItemType } from '../../types/MediaItem'; import type { MediaItemType } from '../../types/MediaItem';
import type { UUIDStringType } from '../../types/UUID';
import { import {
getGroupSizeRecommendedLimit, getGroupSizeRecommendedLimit,
getGroupSizeHardLimit, getGroupSizeHardLimit,
@ -82,7 +83,7 @@ export type ConversationTypeType = typeof ConversationTypes[number];
export type ConversationType = { export type ConversationType = {
id: string; id: string;
uuid?: string; uuid?: UUIDStringType;
e164?: string; e164?: string;
name?: string; name?: string;
familyName?: string; familyName?: string;
@ -134,15 +135,15 @@ export type ConversationType = {
announcementsOnlyReady?: boolean; announcementsOnlyReady?: boolean;
expireTimer?: number; expireTimer?: number;
memberships?: Array<{ memberships?: Array<{
conversationId: string; uuid: UUIDStringType;
isAdmin: boolean; isAdmin: boolean;
}>; }>;
pendingMemberships?: Array<{ pendingMemberships?: Array<{
conversationId: string; uuid: UUIDStringType;
addedByUserId?: string; addedByUserId?: UUIDStringType;
}>; }>;
pendingApprovalMemberships?: Array<{ pendingApprovalMemberships?: Array<{
conversationId: string; uuid: UUIDStringType;
}>; }>;
muteExpiresAt?: number; muteExpiresAt?: number;
dontNotifyForMentionsIfMuted?: boolean; dontNotifyForMentionsIfMuted?: boolean;
@ -294,7 +295,7 @@ type ContactSpoofingReviewStateType =
export type ConversationsStateType = { export type ConversationsStateType = {
preJoinConversation?: PreJoinConversationType; preJoinConversation?: PreJoinConversationType;
invitedConversationIdsForNewlyCreatedGroup?: Array<string>; invitedUuidsForNewlyCreatedGroup?: Array<string>;
conversationLookup: ConversationLookupType; conversationLookup: ConversationLookupType;
conversationsByE164: ConversationLookupType; conversationsByE164: ConversationLookupType;
conversationsByUuid: ConversationLookupType; conversationsByUuid: ConversationLookupType;
@ -373,8 +374,8 @@ type CantAddContactToGroupActionType = {
}; };
}; };
type ClearGroupCreationErrorActionType = { type: 'CLEAR_GROUP_CREATION_ERROR' }; type ClearGroupCreationErrorActionType = { type: 'CLEAR_GROUP_CREATION_ERROR' };
type ClearInvitedConversationsForNewlyCreatedGroupActionType = { type ClearInvitedUuidsForNewlyCreatedGroupActionType = {
type: 'CLEAR_INVITED_CONVERSATIONS_FOR_NEWLY_CREATED_GROUP'; type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP';
}; };
type CloseCantAddContactToGroupModalActionType = { type CloseCantAddContactToGroupModalActionType = {
type: 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL'; type: 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL';
@ -470,7 +471,7 @@ type CreateGroupPendingActionType = {
type CreateGroupFulfilledActionType = { type CreateGroupFulfilledActionType = {
type: 'CREATE_GROUP_FULFILLED'; type: 'CREATE_GROUP_FULFILLED';
payload: { payload: {
invitedConversationIds: Array<string>; invitedUuids: Array<UUIDStringType>;
}; };
}; };
type CreateGroupRejectedActionType = { type CreateGroupRejectedActionType = {
@ -689,7 +690,7 @@ export type ConversationActionType =
| CantAddContactToGroupActionType | CantAddContactToGroupActionType
| ClearChangedMessagesActionType | ClearChangedMessagesActionType
| ClearGroupCreationErrorActionType | ClearGroupCreationErrorActionType
| ClearInvitedConversationsForNewlyCreatedGroupActionType | ClearInvitedUuidsForNewlyCreatedGroupActionType
| ClearSelectedMessageActionType | ClearSelectedMessageActionType
| ClearUnreadMetricsActionType | ClearUnreadMetricsActionType
| CloseCantAddContactToGroupModalActionType | CloseCantAddContactToGroupModalActionType
@ -751,7 +752,7 @@ export const actions = {
cantAddContactToGroup, cantAddContactToGroup,
clearChangedMessages, clearChangedMessages,
clearGroupCreationError, clearGroupCreationError,
clearInvitedConversationsForNewlyCreatedGroup, clearInvitedUuidsForNewlyCreatedGroup,
clearSelectedMessage, clearSelectedMessage,
clearUnreadMetrics, clearUnreadMetrics,
closeCantAddContactToGroupModal, closeCantAddContactToGroupModal,
@ -1320,9 +1321,9 @@ function createGroup(): ThunkAction<
dispatch({ dispatch({
type: 'CREATE_GROUP_FULFILLED', type: 'CREATE_GROUP_FULFILLED',
payload: { payload: {
invitedConversationIds: ( invitedUuids: (conversation.get('pendingMembersV2') || []).map(
conversation.get('pendingMembersV2') || [] member => member.uuid
).map(member => member.conversationId), ),
}, },
}); });
openConversationInternal({ openConversationInternal({
@ -1551,8 +1552,8 @@ function clearChangedMessages(
}, },
}; };
} }
function clearInvitedConversationsForNewlyCreatedGroup(): ClearInvitedConversationsForNewlyCreatedGroupActionType { function clearInvitedUuidsForNewlyCreatedGroup(): ClearInvitedUuidsForNewlyCreatedGroupActionType {
return { type: 'CLEAR_INVITED_CONVERSATIONS_FOR_NEWLY_CREATED_GROUP' }; return { type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP' };
} }
function clearGroupCreationError(): ClearGroupCreationErrorActionType { function clearGroupCreationError(): ClearGroupCreationErrorActionType {
return { type: 'CLEAR_GROUP_CREATION_ERROR' }; return { type: 'CLEAR_GROUP_CREATION_ERROR' };
@ -1979,8 +1980,8 @@ export function reducer(
}; };
} }
if (action.type === 'CLEAR_INVITED_CONVERSATIONS_FOR_NEWLY_CREATED_GROUP') { if (action.type === 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP') {
return omit(state, 'invitedConversationIdsForNewlyCreatedGroup'); return omit(state, 'invitedUuidsForNewlyCreatedGroup');
} }
if (action.type === 'CLEAR_GROUP_CREATION_ERROR') { if (action.type === 'CLEAR_GROUP_CREATION_ERROR') {
@ -2169,8 +2170,7 @@ export function reducer(
// the work. // the work.
return { return {
...state, ...state,
invitedConversationIdsForNewlyCreatedGroup: invitedUuidsForNewlyCreatedGroup: action.payload.invitedUuids,
action.payload.invitedConversationIds,
}; };
} }
if (action.type === 'CREATE_GROUP_REJECTED') { if (action.type === 'CREATE_GROUP_REJECTED') {

View file

@ -6,6 +6,7 @@ import { trigger } from '../../shims/events';
import type { NoopActionType } from './noop'; import type { NoopActionType } from './noop';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import { ThemeType } from '../../types/Util'; import { ThemeType } from '../../types/Util';
import type { UUIDStringType } from '../../types/UUID';
// State // State
@ -15,7 +16,7 @@ export type UserStateType = {
tempPath: string; tempPath: string;
ourConversationId: string; ourConversationId: string;
ourDeviceId: number; ourDeviceId: number;
ourUuid: string; ourUuid: UUIDStringType;
ourNumber: string; ourNumber: string;
platform: string; platform: string;
regionCode: string; regionCode: string;
@ -31,7 +32,7 @@ type UserChangedActionType = {
payload: { payload: {
ourConversationId?: string; ourConversationId?: string;
ourDeviceId?: number; ourDeviceId?: number;
ourUuid?: string; ourUuid?: UUIDStringType;
ourNumber?: string; ourNumber?: string;
regionCode?: string; regionCode?: string;
interactionMode?: 'mouse' | 'keyboard'; interactionMode?: 'mouse' | 'keyboard';
@ -53,7 +54,7 @@ function userChanged(attributes: {
ourConversationId?: string; ourConversationId?: string;
ourDeviceId?: number; ourDeviceId?: number;
ourNumber?: string; ourNumber?: string;
ourUuid?: string; ourUuid?: UUIDStringType;
regionCode?: string; regionCode?: string;
theme?: ThemeType; theme?: ThemeType;
}): UserChangedActionType { }): UserChangedActionType {
@ -81,7 +82,7 @@ export function getEmptyState(): UserStateType {
tempPath: 'missing', tempPath: 'missing',
ourConversationId: 'missing', ourConversationId: 'missing',
ourDeviceId: 0, ourDeviceId: 0,
ourUuid: 'missing', ourUuid: '00000000-0000-4000-8000-000000000000',
ourNumber: 'missing', ourNumber: 'missing',
regionCode: 'missing', regionCode: 'missing',
platform: 'missing', platform: 'missing',

View file

@ -13,6 +13,7 @@ import type {
import { getIncomingCall as getIncomingCallHelper } from '../ducks/calling'; import { getIncomingCall as getIncomingCallHelper } from '../ducks/calling';
import { getUserUuid } from './user'; import { getUserUuid } from './user';
import { getOwn } from '../../util/getOwn'; import { getOwn } from '../../util/getOwn';
import type { UUIDStringType } from '../../types/UUID';
export type CallStateType = DirectCallStateType | GroupCallStateType; export type CallStateType = DirectCallStateType | GroupCallStateType;
@ -61,7 +62,7 @@ export const getIncomingCall = createSelector(
getUserUuid, getUserUuid,
( (
callsByConversation: CallsByConversationType, callsByConversation: CallsByConversationType,
ourUuid: string ourUuid: UUIDStringType
): undefined | DirectCallStateType | GroupCallStateType => ): undefined | DirectCallStateType | GroupCallStateType =>
getIncomingCallHelper(callsByConversation, ourUuid) getIncomingCallHelper(callsByConversation, ourUuid)
); );

View file

@ -27,6 +27,7 @@ import { filterAndSortConversationsByTitle } from '../../util/filterAndSortConve
import type { ContactNameColorType } from '../../types/Colors'; import type { ContactNameColorType } from '../../types/Colors';
import { ContactNameColors } from '../../types/Colors'; import { ContactNameColors } from '../../types/Colors';
import type { AvatarDataType } from '../../types/Avatar'; import type { AvatarDataType } from '../../types/Avatar';
import type { UUIDStringType } from '../../types/UUID';
import { isInSystemContacts } from '../../util/isInSystemContacts'; import { isInSystemContacts } from '../../util/isInSystemContacts';
import { sortByTitle } from '../../util/sortByTitle'; import { sortByTitle } from '../../util/sortByTitle';
import { import {
@ -668,6 +669,12 @@ export const getConversationByIdSelector = createSelector(
getOwn(conversationLookup, id) getOwn(conversationLookup, id)
); );
export const getConversationByUuidSelector = createSelector(
getConversationsByUuid,
conversationsByUuid => (uuid: UUIDStringType): undefined | ConversationType =>
getOwn(conversationsByUuid, uuid)
);
// A little optimization to reset our selector cache whenever high-level application data // A little optimization to reset our selector cache whenever high-level application data
// changes: regionCode and userNumber. // changes: regionCode and userNumber.
export const getCachedSelectorForMessage = createSelector( export const getCachedSelectorForMessage = createSelector(
@ -766,7 +773,7 @@ export const getMessageSelector = createSelector(
conversationSelector: GetConversationByIdType, conversationSelector: GetConversationByIdType,
regionCode: string, regionCode: string,
ourNumber: string, ourNumber: string,
ourUuid: string, ourUuid: UUIDStringType,
ourConversationId: string, ourConversationId: string,
callSelector: CallSelectorType, callSelector: CallSelectorType,
activeCall: undefined | CallStateType, activeCall: undefined | CallStateType,
@ -903,16 +910,13 @@ export const getConversationMessagesSelector = createSelector(
); );
export const getInvitedContactsForNewlyCreatedGroup = createSelector( export const getInvitedContactsForNewlyCreatedGroup = createSelector(
getConversationLookup, getConversationsByUuid,
getConversations, getConversations,
( (
conversationLookup, conversationLookup,
{ invitedConversationIdsForNewlyCreatedGroup = [] } { invitedUuidsForNewlyCreatedGroup = [] }
): Array<ConversationType> => ): Array<ConversationType> =>
deconstructLookup( deconstructLookup(conversationLookup, invitedUuidsForNewlyCreatedGroup)
conversationLookup,
invitedConversationIdsForNewlyCreatedGroup
)
); );
export const getConversationsWithCustomColorSelector = createSelector( export const getConversationsWithCustomColorSelector = createSelector(
@ -962,7 +966,7 @@ export const getGroupAdminsSelector = createSelector(
const admins: Array<ConversationType> = []; const admins: Array<ConversationType> = [];
memberships.forEach(membership => { memberships.forEach(membership => {
if (membership.isAdmin) { if (membership.isAdmin) {
const admin = conversationSelector(membership.conversationId); const admin = conversationSelector(membership.uuid);
admins.push(admin); admins.push(admin);
} }
}); });

View file

@ -37,6 +37,7 @@ import type { PropsType as ProfileChangeNotificationPropsType } from '../../comp
import type { QuotedAttachmentType } from '../../components/conversation/Quote'; import type { QuotedAttachmentType } from '../../components/conversation/Quote';
import { getDomain, isStickerPack } from '../../types/LinkPreview'; import { getDomain, isStickerPack } from '../../types/LinkPreview';
import type { UUIDStringType } from '../../types/UUID';
import type { EmbeddedContactType } from '../../types/EmbeddedContact'; import type { EmbeddedContactType } from '../../types/EmbeddedContact';
import { embeddedContactSelector } from '../../types/EmbeddedContact'; import { embeddedContactSelector } from '../../types/EmbeddedContact';
@ -99,7 +100,7 @@ export type GetPropsForBubbleOptions = Readonly<{
conversationSelector: GetConversationByIdType; conversationSelector: GetConversationByIdType;
ourConversationId: string; ourConversationId: string;
ourNumber?: string; ourNumber?: string;
ourUuid?: string; ourUuid: UUIDStringType;
selectedMessageId?: string; selectedMessageId?: string;
selectedMessageCounter?: number; selectedMessageCounter?: number;
regionCode: string; regionCode: string;
@ -772,7 +773,7 @@ export function isGroupV2Change(message: MessageAttributesType): boolean {
function getPropsForGroupV2Change( function getPropsForGroupV2Change(
message: MessageAttributesType, message: MessageAttributesType,
{ conversationSelector, ourConversationId }: GetPropsForBubbleOptions { conversationSelector, ourUuid }: GetPropsForBubbleOptions
): GroupsV2Props { ): GroupsV2Props {
const change = message.groupV2Change; const change = message.groupV2Change;
@ -784,7 +785,7 @@ function getPropsForGroupV2Change(
return { return {
groupName: conversation?.type === 'group' ? conversation?.name : undefined, groupName: conversation?.type === 'group' ? conversation?.name : undefined,
ourConversationId, ourUuid,
change, change,
}; };
} }
@ -806,7 +807,7 @@ function getPropsForGroupV1Migration(
const droppedGV2MemberIds = message.droppedGV2MemberIds || []; const droppedGV2MemberIds = message.droppedGV2MemberIds || [];
const invitedMembers = invitedGV2Members.map(item => const invitedMembers = invitedGV2Members.map(item =>
conversationSelector(item.conversationId) conversationSelector(item.uuid)
); );
const droppedMembers = droppedGV2MemberIds.map(conversationId => const droppedMembers = droppedGV2MemberIds.map(conversationId =>
conversationSelector(conversationId) conversationSelector(conversationId)
@ -825,7 +826,7 @@ function getPropsForGroupV1Migration(
invitedMembers: rawInvitedMembers, invitedMembers: rawInvitedMembers,
} = migration; } = migration;
const invitedMembers = rawInvitedMembers.map(item => const invitedMembers = rawInvitedMembers.map(item =>
conversationSelector(item.conversationId) conversationSelector(item.uuid)
); );
const droppedMembers = droppedMemberIds.map(conversationId => const droppedMembers = droppedMemberIds.map(conversationId =>
conversationSelector(conversationId) conversationSelector(conversationId)

View file

@ -4,6 +4,7 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import type { LocalizerType, ThemeType } from '../../types/Util'; import type { LocalizerType, ThemeType } from '../../types/Util';
import type { UUIDStringType } from '../../types/UUID';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import type { UserStateType } from '../ducks/user'; import type { UserStateType } from '../ducks/user';
@ -32,7 +33,7 @@ export const getUserConversationId = createSelector(
export const getUserUuid = createSelector( export const getUserUuid = createSelector(
getUser, getUser,
(state: UserStateType): string => state.ourUuid (state: UserStateType): UUIDStringType => state.ourUuid
); );
export const getIntl = createSelector( export const getIntl = createSelector(

View file

@ -17,6 +17,7 @@ import type {
ActiveCallType, ActiveCallType,
GroupCallRemoteParticipantType, GroupCallRemoteParticipantType,
} from '../../types/Calling'; } from '../../types/Calling';
import type { UUIDStringType } from '../../types/UUID';
import { CallMode, CallState } from '../../types/Calling'; import { CallMode, CallState } from '../../types/Calling';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
@ -117,7 +118,7 @@ const mapStateToActiveCallProp = (
} }
const conversationSelectorByUuid = memoize< const conversationSelectorByUuid = memoize<
(uuid: string) => undefined | ConversationType (uuid: UUIDStringType) => undefined | ConversationType
>(uuid => { >(uuid => {
const conversationId = window.ConversationController.ensureContactIds({ const conversationId = window.ConversationController.ensureContactIds({
uuid, uuid,
@ -175,9 +176,9 @@ const mapStateToActiveCallProp = (
const { memberships = [] } = conversation; const { memberships = [] } = conversation;
for (let i = 0; i < memberships.length; i += 1) { for (let i = 0; i < memberships.length; i += 1) {
const { conversationId } = memberships[i]; const { uuid } = memberships[i];
const member = conversationSelectorByUuid(conversationId);
const member = conversationSelector(uuid);
if (!member) { if (!member) {
log.error('Group member has no corresponding conversation'); log.error('Group member has no corresponding conversation');
continue; continue;

View file

@ -26,7 +26,7 @@ const mapStateToProps = (state: StateType): PropsDataType => {
let isAdmin = false; let isAdmin = false;
if (contact && currentConversation && currentConversation.memberships) { if (contact && currentConversation && currentConversation.memberships) {
currentConversation.memberships.forEach(membership => { currentConversation.memberships.forEach(membership => {
if (membership.conversationId === contact.id) { if (membership.uuid === contact.uuid) {
isMember = true; isMember = true;
isAdmin = membership.isAdmin; isAdmin = membership.isAdmin;
} }

View file

@ -10,6 +10,7 @@ import { ConversationDetails } from '../../components/conversation/conversation-
import { import {
getCandidateContactsForNewGroup, getCandidateContactsForNewGroup,
getConversationByIdSelector, getConversationByIdSelector,
getConversationByUuidSelector,
} from '../selectors/conversations'; } from '../selectors/conversations';
import { getGroupMemberships } from '../../util/getGroupMemberships'; import { getGroupMemberships } from '../../util/getGroupMemberships';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
@ -67,6 +68,7 @@ const mapStateToProps = (
Boolean(conversation.groupLink) && Boolean(conversation.groupLink) &&
conversation.accessControlAddFromInviteLink !== ACCESS_ENUM.UNSATISFIABLE; conversation.accessControlAddFromInviteLink !== ACCESS_ENUM.UNSATISFIABLE;
const conversationByUuidSelector = getConversationByUuidSelector(state);
return { return {
...props, ...props,
canEditGroupInfo, canEditGroupInfo,
@ -74,7 +76,7 @@ const mapStateToProps = (
conversation, conversation,
i18n: getIntl(state), i18n: getIntl(state),
isAdmin, isAdmin,
...getGroupMemberships(conversation, conversationSelector), ...getGroupMemberships(conversation, conversationByUuidSelector),
userAvatarData: conversation.avatars || [], userAvatarData: conversation.avatars || [],
hasGroupLink, hasGroupLink,
isGroup: conversation.type === 'group', isGroup: conversation.type === 'group',

View file

@ -8,7 +8,10 @@ import { PendingInvites } from '../../components/conversation/conversation-detai
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { getConversationByIdSelector } from '../selectors/conversations'; import {
getConversationByIdSelector,
getConversationByUuidSelector,
} from '../selectors/conversations';
import { getGroupMemberships } from '../../util/getGroupMemberships'; import { getGroupMemberships } from '../../util/getGroupMemberships';
import { assert } from '../../util/assert'; import { assert } from '../../util/assert';
@ -24,6 +27,7 @@ const mapStateToProps = (
props: SmartPendingInvitesProps props: SmartPendingInvitesProps
): PropsType => { ): PropsType => {
const conversationSelector = getConversationByIdSelector(state); const conversationSelector = getConversationByIdSelector(state);
const conversationByUuidSelector = getConversationByUuidSelector(state);
const conversation = conversationSelector(props.conversationId); const conversation = conversationSelector(props.conversationId);
assert( assert(
@ -33,7 +37,7 @@ const mapStateToProps = (
return { return {
...props, ...props,
...getGroupMemberships(conversation, conversationSelector), ...getGroupMemberships(conversation, conversationByUuidSelector),
conversation, conversation,
i18n: getIntl(state), i18n: getIntl(state),
}; };

View file

@ -20,7 +20,7 @@ import type { ConversationType } from '../ducks/conversations';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { import {
getConversationByIdSelector, getConversationByUuidSelector,
getConversationMessagesSelector, getConversationMessagesSelector,
getConversationSelector, getConversationSelector,
getConversationsByTitleSelector, getConversationsByTitleSelector,
@ -208,11 +208,11 @@ const getWarning = (
return undefined; return undefined;
} }
const getConversationById = getConversationByIdSelector(state); const getConversationByUuid = getConversationByUuidSelector(state);
const { memberships } = getGroupMemberships( const { memberships } = getGroupMemberships(
conversation, conversation,
getConversationById getConversationByUuid
); );
const groupNameCollisions = getCollisionsFromMemberships(memberships); const groupNameCollisions = getCollisionsFromMemberships(memberships);
const hasGroupMembersWithSameName = !isEmpty(groupNameCollisions); const hasGroupMembersWithSameName = !isEmpty(groupNameCollisions);
@ -244,7 +244,7 @@ const getContactSpoofingReview = (
} }
const conversationSelector = getConversationSelector(state); const conversationSelector = getConversationSelector(state);
const getConversationById = getConversationByIdSelector(state); const getConversationByUuid = getConversationByUuidSelector(state);
const currentConversation = conversationSelector(selectedConversationId); const currentConversation = conversationSelector(selectedConversationId);
@ -260,7 +260,7 @@ const getContactSpoofingReview = (
case ContactSpoofingType.MultipleGroupMembersWithSameTitle: { case ContactSpoofingType.MultipleGroupMembersWithSameTitle: {
const { memberships } = getGroupMemberships( const { memberships } = getGroupMemberships(
currentConversation, currentConversation,
getConversationById getConversationByUuid
); );
const groupNameCollisions = getCollisionsFromMemberships(memberships); const groupNameCollisions = getCollisionsFromMemberships(memberships);

View file

@ -4,8 +4,8 @@
import { assert } from 'chai'; import { assert } from 'chai';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { times } from 'lodash'; import { times } from 'lodash';
import { v4 as uuid } from 'uuid';
import * as remoteConfig from '../../RemoteConfig'; import * as remoteConfig from '../../RemoteConfig';
import { UUID } from '../../types/UUID';
import { isConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing'; import { isConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing';
@ -23,7 +23,7 @@ describe('isConversationTooBigToRing', () => {
}); });
const fakeMemberships = (count: number) => const fakeMemberships = (count: number) =>
times(count, () => ({ conversationId: uuid(), isAdmin: false })); times(count, () => ({ uuid: UUID.generate().toString(), isAdmin: false }));
afterEach(() => { afterEach(() => {
sinonSandbox.restore(); sinonSandbox.restore();

View file

@ -4,6 +4,8 @@
import { v4 as generateUuid } from 'uuid'; import { v4 as generateUuid } from 'uuid';
import { sample } from 'lodash'; import { sample } from 'lodash';
import type { ConversationType } from '../../state/ducks/conversations'; import type { ConversationType } from '../../state/ducks/conversations';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import { getRandomColor } from './getRandomColor'; import { getRandomColor } from './getRandomColor';
const FIRST_NAMES = [ const FIRST_NAMES = [
@ -334,7 +336,17 @@ export function getDefaultConversation(
sharedGroupNames: [], sharedGroupNames: [],
title: `${firstName} ${lastName}`, title: `${firstName} ${lastName}`,
type: 'direct' as const, type: 'direct' as const,
uuid: generateUuid(), uuid: UUID.generate().toString(),
...overrideProps, ...overrideProps,
}; };
} }
export function getDefaultConversationWithUuid(
overrideProps: Partial<ConversationType> = {},
uuid: UUIDStringType = UUID.generate().toString()
): ConversationType & { uuid: UUIDStringType } {
return {
...getDefaultConversation(overrideProps),
uuid,
};
}

View file

@ -48,8 +48,13 @@ import { noopAction } from '../../../state/ducks/noop';
import type { StateType } from '../../../state/reducer'; import type { StateType } from '../../../state/reducer';
import { reducer as rootReducer } from '../../../state/reducer'; import { reducer as rootReducer } from '../../../state/reducer';
import { setupI18n } from '../../../util/setupI18n'; import { setupI18n } from '../../../util/setupI18n';
import { UUID } from '../../../types/UUID';
import type { UUIDStringType } from '../../../types/UUID';
import enMessages from '../../../../_locales/en/messages.json'; import enMessages from '../../../../_locales/en/messages.json';
import { getDefaultConversation } from '../../helpers/getDefaultConversation'; import {
getDefaultConversation,
getDefaultConversationWithUuid,
} from '../../helpers/getDefaultConversation';
import { import {
defaultStartDirectConversationComposerState, defaultStartDirectConversationComposerState,
defaultChooseGroupMembersComposerState, defaultChooseGroupMembersComposerState,
@ -69,6 +74,19 @@ describe('both/state/selectors/conversations', () => {
}); });
} }
function makeConversationWithUuid(
id: string
): ConversationType & { uuid: UUIDStringType } {
return getDefaultConversationWithUuid(
{
id,
searchableTitle: `${id} title`,
title: `${id} title`,
},
UUID.fromPrefix(id).toString()
);
}
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
describe('#getConversationByIdSelector', () => { describe('#getConversationByIdSelector', () => {
@ -374,15 +392,17 @@ describe('both/state/selectors/conversations', () => {
}); });
it('returns "hydrated" invited contacts', () => { it('returns "hydrated" invited contacts', () => {
const abc = makeConversationWithUuid('abc');
const def = makeConversationWithUuid('def');
const state = { const state = {
...getEmptyRootState(), ...getEmptyRootState(),
conversations: { conversations: {
...getEmptyState(), ...getEmptyState(),
conversationLookup: { conversationsByUuid: {
abc: makeConversation('abc'), [abc.uuid]: abc,
def: makeConversation('def'), [def.uuid]: def,
}, },
invitedConversationIdsForNewlyCreatedGroup: ['def', 'abc'], invitedUuidsForNewlyCreatedGroup: [def.uuid, abc.uuid],
}, },
}; };
const result = getInvitedContactsForNewlyCreatedGroup(state); const result = getInvitedContactsForNewlyCreatedGroup(state);
@ -1826,23 +1846,17 @@ describe('both/state/selectors/conversations', () => {
}); });
describe('#getContactNameColorSelector', () => { describe('#getContactNameColorSelector', () => {
function makeConversationWithUuid(id: string): ConversationType {
const convo = makeConversation(id);
convo.uuid = id;
return convo;
}
it('returns the right color order sorted by UUID ASC', () => { it('returns the right color order sorted by UUID ASC', () => {
const group = makeConversation('group'); const group = makeConversation('group');
group.type = 'group'; group.type = 'group';
group.sortedGroupMembers = [ group.sortedGroupMembers = [
makeConversationWithUuid('zyx'), makeConversationWithUuid('fff'),
makeConversationWithUuid('vut'), makeConversationWithUuid('f00'),
makeConversationWithUuid('srq'), makeConversationWithUuid('e00'),
makeConversationWithUuid('pon'), makeConversationWithUuid('d00'),
makeConversationWithUuid('mlk'), makeConversationWithUuid('c00'),
makeConversationWithUuid('jih'), makeConversationWithUuid('b00'),
makeConversationWithUuid('gfe'), makeConversationWithUuid('a00'),
]; ];
const state = { const state = {
...getEmptyRootState(), ...getEmptyRootState(),
@ -1856,13 +1870,13 @@ describe('both/state/selectors/conversations', () => {
const contactNameColorSelector = getContactNameColorSelector(state); const contactNameColorSelector = getContactNameColorSelector(state);
assert.equal(contactNameColorSelector('group', 'gfe'), '200'); assert.equal(contactNameColorSelector('group', 'a00'), '200');
assert.equal(contactNameColorSelector('group', 'jih'), '120'); assert.equal(contactNameColorSelector('group', 'b00'), '120');
assert.equal(contactNameColorSelector('group', 'mlk'), '300'); assert.equal(contactNameColorSelector('group', 'c00'), '300');
assert.equal(contactNameColorSelector('group', 'pon'), '010'); assert.equal(contactNameColorSelector('group', 'd00'), '010');
assert.equal(contactNameColorSelector('group', 'srq'), '210'); assert.equal(contactNameColorSelector('group', 'e00'), '210');
assert.equal(contactNameColorSelector('group', 'vut'), '330'); assert.equal(contactNameColorSelector('group', 'f00'), '330');
assert.equal(contactNameColorSelector('group', 'zyx'), '230'); assert.equal(contactNameColorSelector('group', 'fff'), '230');
}); });
it('returns the right colors for direct conversation', () => { it('returns the right colors for direct conversation', () => {

View file

@ -19,7 +19,11 @@ import {
getSearchResults, getSearchResults,
} from '../../../state/selectors/search'; } from '../../../state/selectors/search';
import { makeLookup } from '../../../util/makeLookup'; import { makeLookup } from '../../../util/makeLookup';
import { getDefaultConversation } from '../../helpers/getDefaultConversation'; import { UUID } from '../../../types/UUID';
import {
getDefaultConversation,
getDefaultConversationWithUuid,
} from '../../helpers/getDefaultConversation';
import { ReadStatus } from '../../../messages/MessageReadStatus'; import { ReadStatus } from '../../../messages/MessageReadStatus';
import type { StateType } from '../../../state/reducer'; import type { StateType } from '../../../state/reducer';
@ -52,7 +56,7 @@ describe('both/state/selectors/search', () => {
received_at: NOW, received_at: NOW,
sent_at: NOW, sent_at: NOW,
source: 'source', source: 'source',
sourceUuid: 'sourceUuid', sourceUuid: UUID.generate().toString(),
timestamp: NOW, timestamp: NOW,
type: 'incoming' as const, type: 'incoming' as const,
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
@ -125,10 +129,9 @@ describe('both/state/selectors/search', () => {
it('returns incoming message', () => { it('returns incoming message', () => {
const searchId = 'search-id'; const searchId = 'search-id';
const fromId = 'from-id';
const toId = 'to-id'; const toId = 'to-id';
const from = getDefaultConversation({ id: fromId }); const from = getDefaultConversationWithUuid();
const to = getDefaultConversation({ id: toId }); const to = getDefaultConversation({ id: toId });
const state = { const state = {
@ -136,9 +139,12 @@ describe('both/state/selectors/search', () => {
conversations: { conversations: {
...getEmptyConversationState(), ...getEmptyConversationState(),
conversationLookup: { conversationLookup: {
[fromId]: from, [from.id]: from,
[toId]: to, [toId]: to,
}, },
conversationsByUuid: {
[from.uuid]: from,
},
}, },
search: { search: {
...getEmptySearchState(), ...getEmptySearchState(),
@ -146,7 +152,7 @@ describe('both/state/selectors/search', () => {
[searchId]: { [searchId]: {
...getDefaultMessage(searchId), ...getDefaultMessage(searchId),
type: 'incoming' as const, type: 'incoming' as const,
sourceUuid: fromId, sourceUuid: from.uuid,
conversationId: toId, conversationId: toId,
snippet: 'snippet', snippet: 'snippet',
body: 'snippet', body: 'snippet',
@ -178,11 +184,10 @@ describe('both/state/selectors/search', () => {
it('returns the correct "from" and "to" when sent to me', () => { it('returns the correct "from" and "to" when sent to me', () => {
const searchId = 'search-id'; const searchId = 'search-id';
const fromId = 'from-id';
const toId = fromId;
const myId = 'my-id'; const myId = 'my-id';
const from = getDefaultConversation({ id: fromId }); const from = getDefaultConversationWithUuid();
const toId = from.uuid;
const meAsRecipient = getDefaultConversation({ id: myId }); const meAsRecipient = getDefaultConversation({ id: myId });
const state = { const state = {
@ -190,9 +195,12 @@ describe('both/state/selectors/search', () => {
conversations: { conversations: {
...getEmptyConversationState(), ...getEmptyConversationState(),
conversationLookup: { conversationLookup: {
[fromId]: from, [from.id]: from,
[myId]: meAsRecipient, [myId]: meAsRecipient,
}, },
conversationsByUuid: {
[from.uuid]: from,
},
}, },
ourConversationId: myId, ourConversationId: myId,
search: { search: {
@ -201,7 +209,7 @@ describe('both/state/selectors/search', () => {
[searchId]: { [searchId]: {
...getDefaultMessage(searchId), ...getDefaultMessage(searchId),
type: 'incoming' as const, type: 'incoming' as const,
sourceUuid: fromId, sourceUuid: from.uuid,
conversationId: toId, conversationId: toId,
snippet: 'snippet', snippet: 'snippet',
body: 'snippet', body: 'snippet',
@ -223,24 +231,26 @@ describe('both/state/selectors/search', () => {
it('returns outgoing message and caches appropriately', () => { it('returns outgoing message and caches appropriately', () => {
const searchId = 'search-id'; const searchId = 'search-id';
const fromId = 'from-id';
const toId = 'to-id'; const toId = 'to-id';
const from = getDefaultConversation({ id: fromId }); const from = getDefaultConversationWithUuid();
const to = getDefaultConversation({ id: toId }); const to = getDefaultConversation({ id: toId });
const state = { const state = {
...getEmptyRootState(), ...getEmptyRootState(),
user: { user: {
...getEmptyUserState(), ...getEmptyUserState(),
ourConversationId: fromId, ourConversationId: from.id,
}, },
conversations: { conversations: {
...getEmptyConversationState(), ...getEmptyConversationState(),
conversationLookup: { conversationLookup: {
[fromId]: from, [from.id]: from,
[toId]: to, [toId]: to,
}, },
conversationsByUuid: {
[from.uuid]: from,
},
}, },
search: { search: {
...getEmptySearchState(), ...getEmptySearchState(),
@ -293,9 +303,9 @@ describe('both/state/selectors/search', () => {
...state, ...state,
conversations: { conversations: {
...state.conversations, ...state.conversations,
conversationLookup: { conversationsByUuid: {
...state.conversations.conversationLookup, ...state.conversations.conversationsByUuid,
[fromId]: { [from.uuid]: {
...from, ...from,
name: 'new-name', name: 'new-name',
}, },

View file

@ -0,0 +1,33 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { isValidUuid } from '../../types/UUID';
describe('isValidUuid', () => {
const LOWERCASE_V4_UUID = '9cb737ce-2bb3-4c21-9fe0-d286caa0ca68';
it('returns false for non-strings', () => {
assert.isFalse(isValidUuid(undefined));
assert.isFalse(isValidUuid(null));
assert.isFalse(isValidUuid(1234));
});
it('returns false for non-UUID strings', () => {
assert.isFalse(isValidUuid(''));
assert.isFalse(isValidUuid('hello world'));
assert.isFalse(isValidUuid(` ${LOWERCASE_V4_UUID}`));
assert.isFalse(isValidUuid(`${LOWERCASE_V4_UUID} `));
});
it("returns false for UUIDs that aren't version 4", () => {
assert.isFalse(isValidUuid('a200a6e0-d2d9-11eb-bda7-dd5936a30ddf'));
assert.isFalse(isValidUuid('2adb8b83-4f2c-55ca-a481-7f98b716e615'));
});
it('returns true for v4 UUIDs', () => {
assert.isTrue(isValidUuid(LOWERCASE_V4_UUID));
assert.isTrue(isValidUuid(LOWERCASE_V4_UUID.toUpperCase()));
});
});

View file

@ -3,30 +3,34 @@
import { assert } from 'chai'; import { assert } from 'chai';
import type { ConversationType } from '../../state/ducks/conversations'; import type { ConversationType } from '../../state/ducks/conversations';
import { getDefaultConversation } from '../helpers/getDefaultConversation'; import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import { getDefaultConversationWithUuid } from '../helpers/getDefaultConversation';
import { getGroupMemberships } from '../../util/getGroupMemberships'; import { getGroupMemberships } from '../../util/getGroupMemberships';
describe('getGroupMemberships', () => { describe('getGroupMemberships', () => {
const normalConversation1 = getDefaultConversation(); const normalConversation1 = getDefaultConversationWithUuid();
const normalConversation2 = getDefaultConversation(); const normalConversation2 = getDefaultConversationWithUuid();
const unregisteredConversation = getDefaultConversation({ const unregisteredConversation = getDefaultConversationWithUuid({
discoveredUnregisteredAt: Date.now(), discoveredUnregisteredAt: Date.now(),
}); });
function getConversationById(id: string): undefined | ConversationType { function getConversationByUuid(
uuid: UUIDStringType
): undefined | ConversationType {
return [ return [
normalConversation1, normalConversation1,
normalConversation2, normalConversation2,
unregisteredConversation, unregisteredConversation,
].find(conversation => conversation.id === id); ].find(conversation => conversation.uuid === uuid);
} }
describe('memberships', () => { describe('memberships', () => {
it('returns an empty array if passed undefined', () => { it('returns an empty array if passed undefined', () => {
const conversation = {}; const conversation = {};
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.memberships; .memberships;
assert.isEmpty(result); assert.isEmpty(result);
@ -35,7 +39,7 @@ describe('getGroupMemberships', () => {
it('returns an empty array if passed an empty array', () => { it('returns an empty array if passed an empty array', () => {
const conversation = { memberships: [] }; const conversation = { memberships: [] };
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.memberships; .memberships;
assert.isEmpty(result); assert.isEmpty(result);
@ -45,13 +49,13 @@ describe('getGroupMemberships', () => {
const conversation = { const conversation = {
memberships: [ memberships: [
{ {
conversationId: 'garbage', uuid: UUID.generate().toString(),
isAdmin: true, isAdmin: true,
}, },
], ],
}; };
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.memberships; .memberships;
assert.isEmpty(result); assert.isEmpty(result);
@ -61,13 +65,13 @@ describe('getGroupMemberships', () => {
const conversation = { const conversation = {
memberships: [ memberships: [
{ {
conversationId: unregisteredConversation.id, uuid: unregisteredConversation.uuid,
isAdmin: true, isAdmin: true,
}, },
], ],
}; };
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.memberships; .memberships;
assert.lengthOf(result, 1); assert.lengthOf(result, 1);
@ -81,17 +85,17 @@ describe('getGroupMemberships', () => {
const conversation = { const conversation = {
memberships: [ memberships: [
{ {
conversationId: normalConversation2.id, uuid: normalConversation2.uuid,
isAdmin: false, isAdmin: false,
}, },
{ {
conversationId: normalConversation1.id, uuid: normalConversation1.uuid,
isAdmin: true, isAdmin: true,
}, },
], ],
}; };
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.memberships; .memberships;
assert.lengthOf(result, 2); assert.lengthOf(result, 2);
@ -110,7 +114,7 @@ describe('getGroupMemberships', () => {
it('returns an empty array if passed undefined', () => { it('returns an empty array if passed undefined', () => {
const conversation = {}; const conversation = {};
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingApprovalMemberships; .pendingApprovalMemberships;
assert.isEmpty(result); assert.isEmpty(result);
@ -119,7 +123,7 @@ describe('getGroupMemberships', () => {
it('returns an empty array if passed an empty array', () => { it('returns an empty array if passed an empty array', () => {
const conversation = { pendingApprovalMemberships: [] }; const conversation = { pendingApprovalMemberships: [] };
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingApprovalMemberships; .pendingApprovalMemberships;
assert.isEmpty(result); assert.isEmpty(result);
@ -127,10 +131,10 @@ describe('getGroupMemberships', () => {
it("filters out conversation IDs that don't exist", () => { it("filters out conversation IDs that don't exist", () => {
const conversation = { const conversation = {
pendingApprovalMemberships: [{ conversationId: 'garbage' }], pendingApprovalMemberships: [{ uuid: UUID.generate().toString() }],
}; };
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingApprovalMemberships; .pendingApprovalMemberships;
assert.isEmpty(result); assert.isEmpty(result);
@ -138,12 +142,10 @@ describe('getGroupMemberships', () => {
it('filters out unregistered conversations', () => { it('filters out unregistered conversations', () => {
const conversation = { const conversation = {
pendingApprovalMemberships: [ pendingApprovalMemberships: [{ uuid: unregisteredConversation.uuid }],
{ conversationId: unregisteredConversation.id },
],
}; };
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingApprovalMemberships; .pendingApprovalMemberships;
assert.isEmpty(result); assert.isEmpty(result);
@ -152,12 +154,12 @@ describe('getGroupMemberships', () => {
it('hydrates pending-approval memberships', () => { it('hydrates pending-approval memberships', () => {
const conversation = { const conversation = {
pendingApprovalMemberships: [ pendingApprovalMemberships: [
{ conversationId: normalConversation2.id }, { uuid: normalConversation2.uuid },
{ conversationId: normalConversation1.id }, { uuid: normalConversation1.uuid },
], ],
}; };
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingApprovalMemberships; .pendingApprovalMemberships;
assert.lengthOf(result, 2); assert.lengthOf(result, 2);
@ -170,7 +172,7 @@ describe('getGroupMemberships', () => {
it('returns an empty array if passed undefined', () => { it('returns an empty array if passed undefined', () => {
const conversation = {}; const conversation = {};
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingMemberships; .pendingMemberships;
assert.isEmpty(result); assert.isEmpty(result);
@ -179,7 +181,7 @@ describe('getGroupMemberships', () => {
it('returns an empty array if passed an empty array', () => { it('returns an empty array if passed an empty array', () => {
const conversation = { pendingMemberships: [] }; const conversation = { pendingMemberships: [] };
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingMemberships; .pendingMemberships;
assert.isEmpty(result); assert.isEmpty(result);
@ -188,11 +190,14 @@ describe('getGroupMemberships', () => {
it("filters out conversation IDs that don't exist", () => { it("filters out conversation IDs that don't exist", () => {
const conversation = { const conversation = {
pendingMemberships: [ pendingMemberships: [
{ conversationId: 'garbage', addedByUserId: normalConversation1.id }, {
uuid: UUID.generate().toString(),
addedByUserId: normalConversation1.uuid,
},
], ],
}; };
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingMemberships; .pendingMemberships;
assert.isEmpty(result); assert.isEmpty(result);
@ -202,37 +207,40 @@ describe('getGroupMemberships', () => {
const conversation = { const conversation = {
pendingMemberships: [ pendingMemberships: [
{ {
conversationId: unregisteredConversation.id, uuid: unregisteredConversation.uuid,
addedByUserId: normalConversation1.id, addedByUserId: normalConversation1.uuid,
}, },
], ],
}; };
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingMemberships; .pendingMemberships;
assert.isEmpty(result); assert.isEmpty(result);
}); });
it('hydrates pending memberships', () => { it('hydrates pending memberships', () => {
const abc = UUID.generate().toString();
const xyz = UUID.generate().toString();
const conversation = { const conversation = {
pendingMemberships: [ pendingMemberships: [
{ conversationId: normalConversation2.id, addedByUserId: 'abc' }, { uuid: normalConversation2.uuid, addedByUserId: abc },
{ conversationId: normalConversation1.id, addedByUserId: 'xyz' }, { uuid: normalConversation1.uuid, addedByUserId: xyz },
], ],
}; };
const result = getGroupMemberships(conversation, getConversationById) const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingMemberships; .pendingMemberships;
assert.lengthOf(result, 2); assert.lengthOf(result, 2);
assert.deepEqual(result[0], { assert.deepEqual(result[0], {
member: normalConversation2, member: normalConversation2,
metadata: { addedByUserId: 'abc' }, metadata: { addedByUserId: abc },
}); });
assert.deepEqual(result[1], { assert.deepEqual(result[1], {
member: normalConversation1, member: normalConversation1,
metadata: { addedByUserId: 'xyz' }, metadata: { addedByUserId: xyz },
}); });
}); });
}); });

View file

@ -1,33 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { isValidGuid } from '../../util/isValidGuid';
describe('isValidGuid', () => {
const LOWERCASE_V4_UUID = '9cb737ce-2bb3-4c21-9fe0-d286caa0ca68';
it('returns false for non-strings', () => {
assert.isFalse(isValidGuid(undefined));
assert.isFalse(isValidGuid(null));
assert.isFalse(isValidGuid(1234));
});
it('returns false for non-UUID strings', () => {
assert.isFalse(isValidGuid(''));
assert.isFalse(isValidGuid('hello world'));
assert.isFalse(isValidGuid(` ${LOWERCASE_V4_UUID}`));
assert.isFalse(isValidGuid(`${LOWERCASE_V4_UUID} `));
});
it("returns false for UUIDs that aren't version 4", () => {
assert.isFalse(isValidGuid('a200a6e0-d2d9-11eb-bda7-dd5936a30ddf'));
assert.isFalse(isValidGuid('2adb8b83-4f2c-55ca-a481-7f98b716e615'));
});
it('returns true for v4 UUIDs', () => {
assert.isTrue(isValidGuid(LOWERCASE_V4_UUID));
assert.isTrue(isValidGuid(LOWERCASE_V4_UUID.toUpperCase()));
});
});

View file

@ -10,7 +10,6 @@ import {
SenderKeyRecord, SenderKeyRecord,
SessionRecord, SessionRecord,
} from '@signalapp/signal-client'; } from '@signalapp/signal-client';
import { v4 as getGuid } from 'uuid';
import { signal } from '../protobuf/compiled'; import { signal } from '../protobuf/compiled';
import { sessionStructureToBytes } from '../util/sessionTranslation'; import { sessionStructureToBytes } from '../util/sessionTranslation';
@ -36,8 +35,8 @@ const {
} = signal.proto.storage; } = signal.proto.storage;
describe('SignalProtocolStore', () => { describe('SignalProtocolStore', () => {
const ourUuid = new UUID(getGuid()); const ourUuid = UUID.generate();
const theirUuid = new UUID(getGuid()); const theirUuid = UUID.generate();
let store: SignalProtocolStore; let store: SignalProtocolStore;
let identityKey: KeyPairType; let identityKey: KeyPairType;
let testKey: KeyPairType; let testKey: KeyPairType;
@ -170,7 +169,7 @@ describe('SignalProtocolStore', () => {
describe('senderKeys', () => { describe('senderKeys', () => {
it('roundtrips in memory', async () => { it('roundtrips in memory', async () => {
const distributionId = window.getGuid(); const distributionId = UUID.generate().toString();
const expected = getSenderKeyRecord(); const expected = getSenderKeyRecord();
const deviceId = 1; const deviceId = 1;
@ -200,7 +199,7 @@ describe('SignalProtocolStore', () => {
}); });
it('roundtrips through database', async () => { it('roundtrips through database', async () => {
const distributionId = window.getGuid(); const distributionId = UUID.generate().toString();
const expected = getSenderKeyRecord(); const expected = getSenderKeyRecord();
const deviceId = 1; const deviceId = 1;

View file

@ -3,6 +3,7 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { SendStatus } from '../../messages/MessageSendState'; import { SendStatus } from '../../messages/MessageSendState';
import { UUID } from '../../types/UUID';
describe('Conversations', () => { describe('Conversations', () => {
async function resetConversationController(): Promise<void> { async function resetConversationController(): Promise<void> {
@ -16,14 +17,14 @@ describe('Conversations', () => {
it('updates lastMessage even in race conditions with db', async () => { it('updates lastMessage even in race conditions with db', async () => {
const ourNumber = '+15550000000'; const ourNumber = '+15550000000';
const ourUuid = window.getGuid(); const ourUuid = UUID.generate().toString();
// Creating a fake conversation // Creating a fake conversation
const conversation = new window.Whisper.Conversation({ const conversation = new window.Whisper.Conversation({
avatars: [], avatars: [],
id: window.getGuid(), id: UUID.generate().toString(),
e164: '+15551234567', e164: '+15551234567',
uuid: window.getGuid(), uuid: UUID.generate().toString(),
type: 'private', type: 'private',
inbox_position: 0, inbox_position: 0,
isPinned: false, isPinned: false,
@ -56,7 +57,7 @@ describe('Conversations', () => {
hasAttachments: false, hasAttachments: false,
hasFileAttachments: false, hasFileAttachments: false,
hasVisualMediaAttachments: false, hasVisualMediaAttachments: false,
id: window.getGuid(), id: UUID.generate().toString(),
received_at: now, received_at: now,
sent_at: now, sent_at: now,
timestamp: now, timestamp: now,

View file

@ -10,6 +10,7 @@ import MessageSender from '../../textsecure/SendMessage';
import type { WebAPIType } from '../../textsecure/WebAPI'; import type { WebAPIType } from '../../textsecure/WebAPI';
import type { CallbackResultType } from '../../textsecure/Types.d'; import type { CallbackResultType } from '../../textsecure/Types.d';
import type { StorageAccessType } from '../../types/Storage.d'; import type { StorageAccessType } from '../../types/Storage.d';
import { UUID } from '../../types/UUID';
import { SignalService as Proto } from '../../protobuf'; import { SignalService as Proto } from '../../protobuf';
describe('Message', () => { describe('Message', () => {
@ -32,7 +33,7 @@ describe('Message', () => {
const source = '+1 415-555-5555'; const source = '+1 415-555-5555';
const me = '+14155555556'; const me = '+14155555556';
const ourUuid = window.getGuid(); const ourUuid = UUID.generate().toString();
function createMessage(attrs: { [key: string]: unknown }) { function createMessage(attrs: { [key: string]: unknown }) {
const messages = new window.Whisper.MessageCollection(); const messages = new window.Whisper.MessageCollection();
@ -138,7 +139,7 @@ describe('Message', () => {
const fakeDataMessage = new Uint8Array(0); const fakeDataMessage = new Uint8Array(0);
const conversation1Uuid = conversation1.get('uuid'); const conversation1Uuid = conversation1.get('uuid');
const ignoredUuid = window.getGuid(); const ignoredUuid = UUID.generate().toString();
if (!conversation1Uuid) { if (!conversation1Uuid) {
throw new Error('Test setup failed: conversation1 should have a UUID'); throw new Error('Test setup failed: conversation1 should have a UUID');

View file

@ -12,11 +12,10 @@ import type { MentionCompletionOptions } from '../../../quill/mentions/completio
import { MentionCompletion } from '../../../quill/mentions/completion'; import { MentionCompletion } from '../../../quill/mentions/completion';
import type { ConversationType } from '../../../state/ducks/conversations'; import type { ConversationType } from '../../../state/ducks/conversations';
import { MemberRepository } from '../../../quill/memberRepository'; import { MemberRepository } from '../../../quill/memberRepository';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation'; import { getDefaultConversationWithUuid } from '../../../test-both/helpers/getDefaultConversation';
const me: ConversationType = getDefaultConversation({ const me: ConversationType = getDefaultConversationWithUuid({
id: '666777', id: '666777',
uuid: 'pqrstuv',
title: 'Fred Savage', title: 'Fred Savage',
firstName: 'Fred', firstName: 'Fred',
profileName: 'Fred S.', profileName: 'Fred S.',
@ -28,9 +27,8 @@ const me: ConversationType = getDefaultConversation({
}); });
const members: Array<ConversationType> = [ const members: Array<ConversationType> = [
getDefaultConversation({ getDefaultConversationWithUuid({
id: '555444', id: '555444',
uuid: 'abcdefg',
title: 'Mahershala Ali', title: 'Mahershala Ali',
firstName: 'Mahershala', firstName: 'Mahershala',
profileName: 'Mahershala A.', profileName: 'Mahershala A.',
@ -39,9 +37,8 @@ const members: Array<ConversationType> = [
markedUnread: false, markedUnread: false,
areWeAdmin: false, areWeAdmin: false,
}), }),
getDefaultConversation({ getDefaultConversationWithUuid({
id: '333222', id: '333222',
uuid: 'hijklmno',
title: 'Shia LaBeouf', title: 'Shia LaBeouf',
firstName: 'Shia', firstName: 'Shia',
profileName: 'Shia L.', profileName: 'Shia L.',

View file

@ -2,10 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { v4 as uuid } from 'uuid';
import { times } from 'lodash'; import { times } from 'lodash';
import { ConversationModel } from '../models/conversations'; import { ConversationModel } from '../models/conversations';
import type { ConversationAttributesType } from '../model-types.d'; import type { ConversationAttributesType } from '../model-types.d';
import { UUID } from '../types/UUID';
import { routineProfileRefresh } from '../routineProfileRefresh'; import { routineProfileRefresh } from '../routineProfileRefresh';
import * as getProfileStub from '../util/getProfile'; import * as getProfileStub from '../util/getProfile';
@ -26,12 +26,12 @@ describe('routineProfileRefresh', () => {
overrideAttributes: Partial<ConversationAttributesType> = {} overrideAttributes: Partial<ConversationAttributesType> = {}
): ConversationModel { ): ConversationModel {
const result = new ConversationModel({ const result = new ConversationModel({
accessKey: uuid(), accessKey: UUID.generate().toString(),
active_at: Date.now(), active_at: Date.now(),
draftAttachments: [], draftAttachments: [],
draftBodyRanges: [], draftBodyRanges: [],
draftTimestamp: null, draftTimestamp: null,
id: uuid(), id: UUID.generate().toString(),
inbox_position: 0, inbox_position: 0,
isPinned: false, isPinned: false,
lastMessageDeletedForEveryone: false, lastMessageDeletedForEveryone: false,
@ -43,7 +43,7 @@ describe('routineProfileRefresh', () => {
messageRequestResponseType: 0, messageRequestResponseType: 0,
muteExpiresAt: 0, muteExpiresAt: 0,
profileAvatar: undefined, profileAvatar: undefined,
profileKeyCredential: uuid(), profileKeyCredential: UUID.generate().toString(),
profileKeyVersion: '', profileKeyVersion: '',
profileSharing: true, profileSharing: true,
quotedMessageId: null, quotedMessageId: null,
@ -52,7 +52,7 @@ describe('routineProfileRefresh', () => {
sharedGroupNames: [], sharedGroupNames: [],
timestamp: Date.now(), timestamp: Date.now(),
type: 'private', type: 'private',
uuid: uuid(), uuid: UUID.generate().toString(),
version: 2, version: 2,
...overrideAttributes, ...overrideAttributes,
}); });
@ -85,7 +85,7 @@ describe('routineProfileRefresh', () => {
await routineProfileRefresh({ await routineProfileRefresh({
allConversations: [conversation1, conversation2], allConversations: [conversation1, conversation2],
ourConversationId: uuid(), ourConversationId: UUID.generate().toString(),
storage, storage,
}); });
@ -99,7 +99,7 @@ describe('routineProfileRefresh', () => {
await routineProfileRefresh({ await routineProfileRefresh({
allConversations: [conversation1, conversation2], allConversations: [conversation1, conversation2],
ourConversationId: uuid(), ourConversationId: UUID.generate().toString(),
storage: makeStorage(), storage: makeStorage(),
}); });
@ -124,7 +124,7 @@ describe('routineProfileRefresh', () => {
await routineProfileRefresh({ await routineProfileRefresh({
allConversations: [recentlyActive, inactive, neverActive], allConversations: [recentlyActive, inactive, neverActive],
ourConversationId: uuid(), ourConversationId: UUID.generate().toString(),
storage: makeStorage(), storage: makeStorage(),
}); });
@ -176,7 +176,7 @@ describe('routineProfileRefresh', () => {
await routineProfileRefresh({ await routineProfileRefresh({
allConversations: [neverRefreshed, recentlyFetched], allConversations: [neverRefreshed, recentlyFetched],
ourConversationId: uuid(), ourConversationId: UUID.generate().toString(),
storage: makeStorage(), storage: makeStorage(),
}); });
@ -218,7 +218,7 @@ describe('routineProfileRefresh', () => {
memberWhoHasRecentlyRefreshed, memberWhoHasRecentlyRefreshed,
groupConversation, groupConversation,
], ],
ourConversationId: uuid(), ourConversationId: UUID.generate().toString(),
storage: makeStorage(), storage: makeStorage(),
}); });

View file

@ -23,6 +23,8 @@ import {
GroupCallConnectionState, GroupCallConnectionState,
GroupCallJoinState, GroupCallJoinState,
} from '../../../types/Calling'; } from '../../../types/Calling';
import { UUID } from '../../../types/UUID';
import type { UUIDStringType } from '../../../types/UUID';
describe('calling duck', () => { describe('calling duck', () => {
const stateWithDirectCall: CallingStateType = { const stateWithDirectCall: CallingStateType = {
@ -68,6 +70,11 @@ describe('calling duck', () => {
}, },
}; };
const creatorUuid = UUID.generate().toString();
const differentCreatorUuid = UUID.generate().toString();
const remoteUuid = UUID.generate().toString();
const ringerUuid = UUID.generate().toString();
const stateWithGroupCall = { const stateWithGroupCall = {
...getEmptyState(), ...getEmptyState(),
callsByConversation: { callsByConversation: {
@ -77,15 +84,15 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected, connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined, joinState: GroupCallJoinState.NotJoined,
peekInfo: { peekInfo: {
uuids: ['456'], uuids: [creatorUuid],
creatorUuid: '456', creatorUuid,
eraId: 'xyz', eraId: 'xyz',
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 123, demuxId: 123,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -107,7 +114,7 @@ describe('calling duck', () => {
'fake-group-call-conversation-id' 'fake-group-call-conversation-id'
], ],
ringId: BigInt(123), ringId: BigInt(123),
ringerUuid: '789', ringerUuid: UUID.generate().toString(),
}, },
}, },
}; };
@ -127,7 +134,7 @@ describe('calling duck', () => {
}, },
}; };
const ourUuid = 'ebf5fd79-9344-4ec1-b5c9-af463572caf5'; const ourUuid = UUID.generate().toString();
const getEmptyRootState = () => { const getEmptyRootState = () => {
const rootState = rootReducer(undefined, noopAction()); const rootState = rootReducer(undefined, noopAction());
@ -607,7 +614,7 @@ describe('calling duck', () => {
], ],
connectionState: GroupCallConnectionState.NotConnected, connectionState: GroupCallConnectionState.NotConnected,
ringId: BigInt(123), ringId: BigInt(123),
ringerUuid: '789', ringerUuid: UUID.generate().toString(),
}, },
}, },
}; };
@ -899,15 +906,15 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
peekInfo: { peekInfo: {
uuids: ['456'], uuids: [creatorUuid],
creatorUuid: '456', creatorUuid,
eraId: 'xyz', eraId: 'xyz',
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 123, demuxId: 123,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -927,15 +934,15 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected, connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joining, joinState: GroupCallJoinState.Joining,
peekInfo: { peekInfo: {
uuids: ['456'], uuids: [creatorUuid],
creatorUuid: '456', creatorUuid,
eraId: 'xyz', eraId: 'xyz',
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 123, demuxId: 123,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1022,7 +1029,7 @@ describe('calling duck', () => {
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 456, demuxId: 456,
hasRemoteAudio: false, hasRemoteAudio: false,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1048,7 +1055,7 @@ describe('calling duck', () => {
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 456, demuxId: 456,
hasRemoteAudio: false, hasRemoteAudio: false,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1071,7 +1078,7 @@ describe('calling duck', () => {
'fake-group-call-conversation-id' 'fake-group-call-conversation-id'
], ],
ringId: BigInt(456), ringId: BigInt(456),
ringerUuid: '55addfd8-09ed-4f5b-b42e-01058898d13b', ringerUuid,
}, },
}, },
}; };
@ -1090,7 +1097,7 @@ describe('calling duck', () => {
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 456, demuxId: 456,
hasRemoteAudio: false, hasRemoteAudio: false,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1107,7 +1114,7 @@ describe('calling duck', () => {
{ {
callMode: CallMode.Group, callMode: CallMode.Group,
ringId: BigInt(456), ringId: BigInt(456),
ringerUuid: '55addfd8-09ed-4f5b-b42e-01058898d13b', ringerUuid,
} }
); );
}); });
@ -1122,7 +1129,7 @@ describe('calling duck', () => {
'fake-group-call-conversation-id' 'fake-group-call-conversation-id'
], ],
ringId: BigInt(456), ringId: BigInt(456),
ringerUuid: '55addfd8-09ed-4f5b-b42e-01058898d13b', ringerUuid,
}, },
}, },
}; };
@ -1141,7 +1148,7 @@ describe('calling duck', () => {
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 456, demuxId: 456,
hasRemoteAudio: false, hasRemoteAudio: false,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1179,7 +1186,7 @@ describe('calling duck', () => {
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 456, demuxId: 456,
hasRemoteAudio: false, hasRemoteAudio: false,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1210,7 +1217,7 @@ describe('calling duck', () => {
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 456, demuxId: 456,
hasRemoteAudio: false, hasRemoteAudio: false,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1251,7 +1258,7 @@ describe('calling duck', () => {
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 456, demuxId: 456,
hasRemoteAudio: false, hasRemoteAudio: false,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1423,7 +1430,7 @@ describe('calling duck', () => {
const action = receiveIncomingGroupCall({ const action = receiveIncomingGroupCall({
conversationId: 'fake-group-call-conversation-id', conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(456), ringId: BigInt(456),
ringerUuid: '208b8ce6-3a73-48ee-9c8a-32e6196f6e96', ringerUuid,
}); });
const result = reducer(stateWithIncomingGroupCall, action); const result = reducer(stateWithIncomingGroupCall, action);
@ -1446,7 +1453,7 @@ describe('calling duck', () => {
const action = receiveIncomingGroupCall({ const action = receiveIncomingGroupCall({
conversationId: 'fake-group-call-conversation-id', conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(456), ringId: BigInt(456),
ringerUuid: '208b8ce6-3a73-48ee-9c8a-32e6196f6e96', ringerUuid,
}); });
const result = reducer(state, action); const result = reducer(state, action);
@ -1457,7 +1464,7 @@ describe('calling duck', () => {
const action = receiveIncomingGroupCall({ const action = receiveIncomingGroupCall({
conversationId: 'fake-group-call-conversation-id', conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(456), ringId: BigInt(456),
ringerUuid: '208b8ce6-3a73-48ee-9c8a-32e6196f6e96', ringerUuid,
}); });
const result = reducer(getEmptyState(), action); const result = reducer(getEmptyState(), action);
@ -1475,7 +1482,7 @@ describe('calling duck', () => {
}, },
remoteParticipants: [], remoteParticipants: [],
ringId: BigInt(456), ringId: BigInt(456),
ringerUuid: '208b8ce6-3a73-48ee-9c8a-32e6196f6e96', ringerUuid,
} }
); );
}); });
@ -1484,7 +1491,7 @@ describe('calling duck', () => {
const action = receiveIncomingGroupCall({ const action = receiveIncomingGroupCall({
conversationId: 'fake-group-call-conversation-id', conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(456), ringId: BigInt(456),
ringerUuid: '208b8ce6-3a73-48ee-9c8a-32e6196f6e96', ringerUuid,
}); });
const result = reducer(stateWithGroupCall, action); const result = reducer(stateWithGroupCall, action);
@ -1492,7 +1499,7 @@ describe('calling duck', () => {
result.callsByConversation['fake-group-call-conversation-id'], result.callsByConversation['fake-group-call-conversation-id'],
{ {
ringId: BigInt(456), ringId: BigInt(456),
ringerUuid: '208b8ce6-3a73-48ee-9c8a-32e6196f6e96', ringerUuid,
} }
); );
}); });
@ -1644,15 +1651,15 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected, connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined, joinState: GroupCallJoinState.NotJoined,
peekInfo: { peekInfo: {
uuids: ['456'], uuids: [creatorUuid],
creatorUuid: '456', creatorUuid,
eraId: 'xyz', eraId: 'xyz',
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 123, demuxId: 123,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1670,15 +1677,15 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected, connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined, joinState: GroupCallJoinState.NotJoined,
peekInfo: { peekInfo: {
uuids: ['456'], uuids: [creatorUuid],
creatorUuid: '456', creatorUuid,
eraId: 'xyz', eraId: 'xyz',
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 123, demuxId: 123,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1734,7 +1741,7 @@ describe('calling duck', () => {
peekInfo: undefined, peekInfo: undefined,
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 123, demuxId: 123,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1749,8 +1756,8 @@ describe('calling duck', () => {
const call = const call =
result.callsByConversation['fake-group-call-conversation-id']; result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, { assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: ['456'], uuids: [creatorUuid],
creatorUuid: '456', creatorUuid,
eraId: 'xyz', eraId: 'xyz',
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
@ -1769,15 +1776,15 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected, connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined, joinState: GroupCallJoinState.NotJoined,
peekInfo: { peekInfo: {
uuids: ['999'], uuids: [differentCreatorUuid],
creatorUuid: '999', creatorUuid: differentCreatorUuid,
eraId: 'abc', eraId: 'abc',
maxDevices: 5, maxDevices: 5,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 123, demuxId: 123,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1792,8 +1799,8 @@ describe('calling duck', () => {
const call = const call =
result.callsByConversation['fake-group-call-conversation-id']; result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, { assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: ['999'], uuids: [differentCreatorUuid],
creatorUuid: '999', creatorUuid: differentCreatorUuid,
eraId: 'abc', eraId: 'abc',
maxDevices: 5, maxDevices: 5,
deviceCount: 1, deviceCount: 1,
@ -1810,7 +1817,7 @@ describe('calling duck', () => {
'fake-group-call-conversation-id' 'fake-group-call-conversation-id'
], ],
ringId: BigInt(987), ringId: BigInt(987),
ringerUuid: 'd59f05f7-3be8-4d44-a1e8-0d7cb5677ed8', ringerUuid,
}, },
}, },
}, },
@ -1825,7 +1832,7 @@ describe('calling duck', () => {
peekInfo: undefined, peekInfo: undefined,
remoteParticipants: [ remoteParticipants: [
{ {
uuid: '123', uuid: remoteUuid,
demuxId: 123, demuxId: 123,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -1844,10 +1851,7 @@ describe('calling duck', () => {
} }
assert.strictEqual(call.ringId, BigInt(987)); assert.strictEqual(call.ringId, BigInt(987));
assert.strictEqual( assert.strictEqual(call.ringerUuid, ringerUuid);
call.ringerUuid,
'd59f05f7-3be8-4d44-a1e8-0d7cb5677ed8'
);
}); });
}); });
@ -2036,47 +2040,33 @@ describe('calling duck', () => {
}); });
describe('isAnybodyElseInGroupCall', () => { describe('isAnybodyElseInGroupCall', () => {
const fakePeekInfo = (uuids: Array<string>) => ({ const fakePeekInfo = (uuids: Array<UUIDStringType>) => ({
uuids, uuids,
maxDevices: 5, maxDevices: 5,
deviceCount: uuids.length, deviceCount: uuids.length,
}); });
it('returns false if the peek info has no participants', () => { it('returns false if the peek info has no participants', () => {
assert.isFalse( assert.isFalse(isAnybodyElseInGroupCall(fakePeekInfo([]), remoteUuid));
isAnybodyElseInGroupCall(
fakePeekInfo([]),
'2cd7b14c-3433-4b3c-9685-1ef1e2d26db2'
)
);
}); });
it('returns false if the peek info has one participant, you', () => { it('returns false if the peek info has one participant, you', () => {
assert.isFalse( assert.isFalse(
isAnybodyElseInGroupCall( isAnybodyElseInGroupCall(fakePeekInfo([creatorUuid]), creatorUuid)
fakePeekInfo(['2cd7b14c-3433-4b3c-9685-1ef1e2d26db2']),
'2cd7b14c-3433-4b3c-9685-1ef1e2d26db2'
)
); );
}); });
it('returns true if the peek info has one participant, someone else', () => { it('returns true if the peek info has one participant, someone else', () => {
assert.isTrue( assert.isTrue(
isAnybodyElseInGroupCall( isAnybodyElseInGroupCall(fakePeekInfo([creatorUuid]), remoteUuid)
fakePeekInfo(['ca0ae16c-2936-4c68-86b1-a6f82e8fe67f']),
'2cd7b14c-3433-4b3c-9685-1ef1e2d26db2'
)
); );
}); });
it('returns true if the peek info has two participants, you and someone else', () => { it('returns true if the peek info has two participants, you and someone else', () => {
assert.isTrue( assert.isTrue(
isAnybodyElseInGroupCall( isAnybodyElseInGroupCall(
fakePeekInfo([ fakePeekInfo([creatorUuid, remoteUuid]),
'ca0ae16c-2936-4c68-86b1-a6f82e8fe67f', remoteUuid
'2cd7b14c-3433-4b3c-9685-1ef1e2d26db2',
]),
'2cd7b14c-3433-4b3c-9685-1ef1e2d26db2'
) )
); );
}); });

View file

@ -28,8 +28,12 @@ import {
import { ReadStatus } from '../../../messages/MessageReadStatus'; import { ReadStatus } from '../../../messages/MessageReadStatus';
import { ContactSpoofingType } from '../../../util/contactSpoofing'; import { ContactSpoofingType } from '../../../util/contactSpoofing';
import { CallMode } from '../../../types/Calling'; import { CallMode } from '../../../types/Calling';
import { UUID } from '../../../types/UUID';
import * as groups from '../../../groups'; import * as groups from '../../../groups';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation'; import {
getDefaultConversation,
getDefaultConversationWithUuid,
} from '../../../test-both/helpers/getDefaultConversation';
import { getDefaultAvatars } from '../../../types/Avatar'; import { getDefaultAvatars } from '../../../types/Avatar';
import { import {
defaultStartDirectConversationComposerState, defaultStartDirectConversationComposerState,
@ -40,7 +44,7 @@ import {
const { const {
cantAddContactToGroup, cantAddContactToGroup,
clearGroupCreationError, clearGroupCreationError,
clearInvitedConversationsForNewlyCreatedGroup, clearInvitedUuidsForNewlyCreatedGroup,
closeCantAddContactToGroupModal, closeCantAddContactToGroupModal,
closeContactSpoofingReview, closeContactSpoofingReview,
closeMaximumGroupSizeModal, closeMaximumGroupSizeModal,
@ -225,26 +229,24 @@ describe('both/state/ducks/conversations', () => {
}); });
it('adds and removes uuid-only contact', () => { it('adds and removes uuid-only contact', () => {
const removed = getDefaultConversation({ const removed = getDefaultConversationWithUuid({
id: 'id-removed', id: 'id-removed',
uuid: 'uuid-removed',
e164: undefined, e164: undefined,
}); });
const state = { const state = {
...getEmptyState(), ...getEmptyState(),
conversationsByuuid: { conversationsByuuid: {
'uuid-removed': removed, [removed.uuid]: removed,
}, },
}; };
const added = getDefaultConversation({ const added = getDefaultConversationWithUuid({
id: 'id-added', id: 'id-added',
uuid: 'uuid-added',
e164: undefined, e164: undefined,
}); });
const expected = { const expected = {
'uuid-added': added, [added.uuid]: added,
}; };
const actual = updateConversationLookups(added, removed, state); const actual = updateConversationLookups(added, removed, state);
@ -307,6 +309,7 @@ describe('both/state/ducks/conversations', () => {
const messageId = 'message-guid-1'; const messageId = 'message-guid-1';
const messageIdTwo = 'message-guid-2'; const messageIdTwo = 'message-guid-2';
const messageIdThree = 'message-guid-3'; const messageIdThree = 'message-guid-3';
const sourceUuid = UUID.generate().toString();
function getDefaultMessage(id: string): MessageType { function getDefaultMessage(id: string): MessageType {
return { return {
@ -316,7 +319,7 @@ describe('both/state/ducks/conversations', () => {
received_at: previousTime, received_at: previousTime,
sent_at: previousTime, sent_at: previousTime,
source: 'source', source: 'source',
sourceUuid: 'sourceUuid', sourceUuid,
timestamp: previousTime, timestamp: previousTime,
type: 'incoming' as const, type: 'incoming' as const,
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
@ -491,16 +494,19 @@ describe('both/state/ducks/conversations', () => {
}); });
}); });
describe('CLEAR_INVITED_CONVERSATIONS_FOR_NEWLY_CREATED_GROUP', () => { describe('CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP', () => {
it('clears the list of invited conversation IDs', () => { it('clears the list of invited conversation UUIDs', () => {
const state = { const state = {
...getEmptyState(), ...getEmptyState(),
invitedConversationIdsForNewlyCreatedGroup: ['abc123', 'def456'], invitedUuidsForNewlyCreatedGroup: [
UUID.generate().toString(),
UUID.generate().toString(),
],
}; };
const action = clearInvitedConversationsForNewlyCreatedGroup(); const action = clearInvitedUuidsForNewlyCreatedGroup();
const result = reducer(state, action); const result = reducer(state, action);
assert.isUndefined(result.invitedConversationIdsForNewlyCreatedGroup); assert.isUndefined(result.invitedUuidsForNewlyCreatedGroup);
}); });
}); });
@ -775,13 +781,14 @@ describe('both/state/ducks/conversations', () => {
}); });
it('dispatches a CREATE_GROUP_FULFILLED event (which updates the newly-created conversation IDs), triggers a showConversation event and switches to the associated conversation on success', async () => { it('dispatches a CREATE_GROUP_FULFILLED event (which updates the newly-created conversation IDs), triggers a showConversation event and switches to the associated conversation on success', async () => {
const abc = UUID.fromPrefix('abc').toString();
createGroupStub.resolves({ createGroupStub.resolves({
id: '9876', id: '9876',
get: (key: string) => { get: (key: string) => {
if (key !== 'pendingMembersV2') { if (key !== 'pendingMembersV2') {
throw new Error('This getter is not set up for this test'); throw new Error('This getter is not set up for this test');
} }
return [{ conversationId: 'xyz999' }]; return [{ uuid: abc }];
}, },
}); });
@ -805,14 +812,12 @@ describe('both/state/ducks/conversations', () => {
sinon.assert.calledWith(dispatch, { sinon.assert.calledWith(dispatch, {
type: 'CREATE_GROUP_FULFILLED', type: 'CREATE_GROUP_FULFILLED',
payload: { invitedConversationIds: ['xyz999'] }, payload: { invitedUuids: [abc] },
}); });
const fulfilledAction = dispatch.getCall(1).args[0]; const fulfilledAction = dispatch.getCall(1).args[0];
const result = reducer(conversationsState, fulfilledAction); const result = reducer(conversationsState, fulfilledAction);
assert.deepEqual(result.invitedConversationIdsForNewlyCreatedGroup, [ assert.deepEqual(result.invitedUuidsForNewlyCreatedGroup, [abc]);
'xyz999',
]);
sinon.assert.calledWith(dispatch, { sinon.assert.calledWith(dispatch, {
type: 'SWITCH_TO_ASSOCIATED_VIEW', type: 'SWITCH_TO_ASSOCIATED_VIEW',
@ -1765,14 +1770,12 @@ describe('both/state/ducks/conversations', () => {
}); });
describe('COLORS_CHANGED', () => { describe('COLORS_CHANGED', () => {
const abc = getDefaultConversation({ const abc = getDefaultConversationWithUuid({
id: 'abc', id: 'abc',
uuid: 'abc',
conversationColor: 'wintergreen', conversationColor: 'wintergreen',
}); });
const def = getDefaultConversation({ const def = getDefaultConversationWithUuid({
id: 'def', id: 'def',
uuid: 'def',
conversationColor: 'infrared', conversationColor: 'infrared',
}); });
const ghi = getDefaultConversation({ const ghi = getDefaultConversation({
@ -1820,8 +1823,12 @@ describe('both/state/ducks/conversations', () => {
assert.isUndefined(nextState.conversationLookup.def.conversationColor); assert.isUndefined(nextState.conversationLookup.def.conversationColor);
assert.isUndefined(nextState.conversationLookup.ghi.conversationColor); assert.isUndefined(nextState.conversationLookup.ghi.conversationColor);
assert.isUndefined(nextState.conversationLookup.jkl.conversationColor); assert.isUndefined(nextState.conversationLookup.jkl.conversationColor);
assert.isUndefined(nextState.conversationsByUuid.abc.conversationColor); assert.isUndefined(
assert.isUndefined(nextState.conversationsByUuid.def.conversationColor); nextState.conversationsByUuid[abc.uuid].conversationColor
);
assert.isUndefined(
nextState.conversationsByUuid[def.uuid].conversationColor
);
assert.isUndefined(nextState.conversationsByE164.ghi.conversationColor); assert.isUndefined(nextState.conversationsByE164.ghi.conversationColor);
assert.isUndefined( assert.isUndefined(
nextState.conversationsByGroupId.jkl.conversationColor nextState.conversationsByGroupId.jkl.conversationColor

View file

@ -5,10 +5,10 @@
import { assert } from 'chai'; import { assert } from 'chai';
import sinon from 'sinon'; import sinon from 'sinon';
import { v4 as uuid } from 'uuid';
import { ConversationModel } from '../models/conversations'; import { ConversationModel } from '../models/conversations';
import type { ConversationAttributesType } from '../model-types.d'; import type { ConversationAttributesType } from '../model-types.d';
import type SendMessage from '../textsecure/SendMessage'; import type SendMessage from '../textsecure/SendMessage';
import { UUID } from '../types/UUID';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup'; import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
@ -41,7 +41,7 @@ describe('updateConversationsWithUuidLookup', () => {
'FakeConversationController is not set up for this case (E164 must be provided)' 'FakeConversationController is not set up for this case (E164 must be provided)'
); );
assert( assert(
uuid, uuidFromServer,
'FakeConversationController is not set up for this case (UUID must be provided)' 'FakeConversationController is not set up for this case (UUID must be provided)'
); );
assert( assert(
@ -81,7 +81,7 @@ describe('updateConversationsWithUuidLookup', () => {
attributes: Readonly<Partial<ConversationAttributesType>> = {} attributes: Readonly<Partial<ConversationAttributesType>> = {}
): ConversationModel { ): ConversationModel {
return new ConversationModel({ return new ConversationModel({
id: uuid(), id: UUID.generate().toString(),
inbox_position: 0, inbox_position: 0,
isPinned: false, isPinned: false,
lastMessageDeletedForEveryone: false, lastMessageDeletedForEveryone: false,
@ -128,7 +128,7 @@ describe('updateConversationsWithUuidLookup', () => {
conversationController: new FakeConversationController(), conversationController: new FakeConversationController(),
conversations: [ conversations: [
createConversation(), createConversation(),
createConversation({ uuid: uuid() }), createConversation({ uuid: UUID.generate().toString() }),
], ],
messaging: fakeMessaging, messaging: fakeMessaging,
}); });
@ -140,11 +140,11 @@ describe('updateConversationsWithUuidLookup', () => {
const conversation1 = createConversation({ e164: '+13215559876' }); const conversation1 = createConversation({ e164: '+13215559876' });
const conversation2 = createConversation({ const conversation2 = createConversation({
e164: '+16545559876', e164: '+16545559876',
uuid: 'should be overwritten', uuid: UUID.generate().toString(), // should be overwritten
}); });
const uuid1 = uuid(); const uuid1 = UUID.generate().toString();
const uuid2 = uuid(); const uuid2 = UUID.generate().toString();
fakeGetUuidsForE164s.resolves({ fakeGetUuidsForE164s.resolves({
'+13215559876': uuid1, '+13215559876': uuid1,
@ -187,7 +187,7 @@ describe('updateConversationsWithUuidLookup', () => {
}); });
it("doesn't mark conversations unregistered if we already had a UUID for them, even if the server doesn't return one", async () => { it("doesn't mark conversations unregistered if we already had a UUID for them, even if the server doesn't return one", async () => {
const existingUuid = uuid(); const existingUuid = UUID.generate().toString();
const conversation = createConversation({ const conversation = createConversation({
e164: '+13215559876', e164: '+13215559876',
uuid: existingUuid, uuid: existingUuid,

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai'; import { assert } from 'chai';
import { v4 as uuid } from 'uuid';
import * as Bytes from '../../Bytes'; import * as Bytes from '../../Bytes';
import { import {
@ -11,6 +10,7 @@ import {
decryptProfileName, decryptProfileName,
decryptProfile, decryptProfile,
} from '../../Crypto'; } from '../../Crypto';
import { UUID } from '../../types/UUID';
import { encryptProfileData } from '../../util/encryptProfileData'; import { encryptProfileData } from '../../util/encryptProfileData';
describe('encryptProfileData', () => { describe('encryptProfileData', () => {
@ -22,7 +22,7 @@ describe('encryptProfileData', () => {
familyName: 'Kid', familyName: 'Kid',
firstName: 'Zombie', firstName: 'Zombie',
profileKey: Bytes.toBase64(keyBuffer), profileKey: Bytes.toBase64(keyBuffer),
uuid: uuid(), uuid: UUID.generate().toString(),
// To satisfy TS // To satisfy TS
acceptedMessageRequest: true, acceptedMessageRequest: true,

View file

@ -5,11 +5,10 @@ import { assert } from 'chai';
import type { ConversationType } from '../../state/ducks/conversations'; import type { ConversationType } from '../../state/ducks/conversations';
import { MemberRepository } from '../../quill/memberRepository'; import { MemberRepository } from '../../quill/memberRepository';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation'; import { getDefaultConversationWithUuid } from '../../test-both/helpers/getDefaultConversation';
const memberMahershala: ConversationType = getDefaultConversation({ const memberMahershala: ConversationType = getDefaultConversationWithUuid({
id: '555444', id: '555444',
uuid: 'abcdefg',
title: 'Pal', title: 'Pal',
firstName: 'Mahershala', firstName: 'Mahershala',
profileName: 'Mr Ali', profileName: 'Mr Ali',
@ -20,9 +19,8 @@ const memberMahershala: ConversationType = getDefaultConversation({
areWeAdmin: false, areWeAdmin: false,
}); });
const memberShia: ConversationType = getDefaultConversation({ const memberShia: ConversationType = getDefaultConversationWithUuid({
id: '333222', id: '333222',
uuid: 'hijklmno',
title: 'Buddy', title: 'Buddy',
firstName: 'Shia', firstName: 'Shia',
profileName: 'Sr LaBeouf', profileName: 'Sr LaBeouf',
@ -35,9 +33,8 @@ const memberShia: ConversationType = getDefaultConversation({
const members: Array<ConversationType> = [memberMahershala, memberShia]; const members: Array<ConversationType> = [memberMahershala, memberShia];
const singleMember: ConversationType = getDefaultConversation({ const singleMember: ConversationType = getDefaultConversationWithUuid({
id: '666777', id: '666777',
uuid: 'pqrstuv',
title: 'The Guy', title: 'The Guy',
firstName: 'Jeff', firstName: 'Jeff',
profileName: 'Jr Klaus', profileName: 'Jr Klaus',
@ -85,7 +82,7 @@ describe('MemberRepository', () => {
it('returns a matched member', () => { it('returns a matched member', () => {
const memberRepository = new MemberRepository(members); const memberRepository = new MemberRepository(members);
assert.isDefined(memberRepository.getMemberByUuid('abcdefg')); assert.isDefined(memberRepository.getMemberByUuid(memberMahershala.uuid));
}); });
it('returns undefined when it does not have the member', () => { it('returns undefined when it does not have the member', () => {

View file

@ -8,7 +8,7 @@ import Delta from 'quill-delta';
import { matchMention } from '../../../quill/mentions/matchers'; import { matchMention } from '../../../quill/mentions/matchers';
import { MemberRepository } from '../../../quill/memberRepository'; import { MemberRepository } from '../../../quill/memberRepository';
import type { ConversationType } from '../../../state/ducks/conversations'; import type { ConversationType } from '../../../state/ducks/conversations';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation'; import { getDefaultConversationWithUuid } from '../../../test-both/helpers/getDefaultConversation';
class FakeTokenList<T> extends Array<T> { class FakeTokenList<T> extends Array<T> {
constructor(elements: Array<T>) { constructor(elements: Array<T>) {
@ -38,9 +38,8 @@ const createMockMentionBlotElement = (
dataset: Record<string, string> dataset: Record<string, string>
): HTMLElement => createMockElement('mention-blot', dataset); ): HTMLElement => createMockElement('mention-blot', dataset);
const memberMahershala: ConversationType = getDefaultConversation({ const memberMahershala: ConversationType = getDefaultConversationWithUuid({
id: '555444', id: '555444',
uuid: 'abcdefg',
title: 'Mahershala Ali', title: 'Mahershala Ali',
firstName: 'Mahershala', firstName: 'Mahershala',
profileName: 'Mahershala A.', profileName: 'Mahershala A.',
@ -50,9 +49,8 @@ const memberMahershala: ConversationType = getDefaultConversation({
areWeAdmin: false, areWeAdmin: false,
}); });
const memberShia: ConversationType = getDefaultConversation({ const memberShia: ConversationType = getDefaultConversationWithUuid({
id: '333222', id: '333222',
uuid: 'hijklmno',
title: 'Shia LaBeouf', title: 'Shia LaBeouf',
firstName: 'Shia', firstName: 'Shia',
profileName: 'Shia L.', profileName: 'Shia L.',

View file

@ -6,7 +6,8 @@ import type { Database } from 'better-sqlite3';
import SQL from 'better-sqlite3'; import SQL from 'better-sqlite3';
import { v4 as generateGuid } from 'uuid'; import { v4 as generateGuid } from 'uuid';
import { SCHEMA_VERSIONS } from '../sql/Server'; import { SCHEMA_VERSIONS } from '../sql/migrations';
import { consoleLogger } from '../util/consoleLogger';
const OUR_UUID = generateGuid(); const OUR_UUID = generateGuid();
@ -17,7 +18,7 @@ describe('SQL migrations test', () => {
const startVersion = db.pragma('user_version', { simple: true }); const startVersion = db.pragma('user_version', { simple: true });
for (const run of SCHEMA_VERSIONS) { for (const run of SCHEMA_VERSIONS) {
run(startVersion, db); run(startVersion, db, consoleLogger);
const currentVersion = db.pragma('user_version', { simple: true }); const currentVersion = db.pragma('user_version', { simple: true });
@ -616,4 +617,157 @@ describe('SQL migrations test', () => {
assert.sameDeepMembers(reactionMessageIds, [MESSAGE_ID_2, MESSAGE_ID_3]); assert.sameDeepMembers(reactionMessageIds, [MESSAGE_ID_2, MESSAGE_ID_3]);
}); });
}); });
describe('updateToSchemaVersion43', () => {
it('remaps conversation ids to UUIDs in groups and messages', () => {
updateToVersion(42);
const UUID_A = generateGuid();
const UUID_B = generateGuid();
const UUID_C = generateGuid();
const rawConvoC = {
id: 'c',
uuid: UUID_C,
membersV2: [
{ conversationId: 'a', joinedAtVersion: 1 },
{ conversationId: 'b', joinedAtVersion: 2 },
{ conversationId: 'z', joinedAtVersion: 3 },
],
pendingMembersV2: [
{ conversationId: 'a', addedByUserId: 'b', timestamp: 4 },
{ conversationId: 'b', addedByUserId: UUID_A, timestamp: 5 },
{ conversationId: 'z', timestamp: 6 },
],
pendingAdminApprovalV2: [
{ conversationId: 'a', timestamp: 6 },
{ conversationId: 'b', timestamp: 7 },
{ conversationId: 'z', timestamp: 8 },
],
};
const CHANGE_TYPES = [
'member-add',
'member-add-from-link',
'member-add-from-admin-approval',
'member-privilege',
'member-remove',
'pending-add-one',
'admin-approval-add-one',
];
const CHANGE_TYPES_WITH_INVITER = [
'member-add-from-invite',
'pending-remove-one',
'pending-remove-many',
'admin-approval-remove-one',
];
db.exec(
`
INSERT INTO conversations
(id, uuid, json)
VALUES
('a', '${UUID_A}', '${JSON.stringify({ id: 'a', uuid: UUID_A })}'),
('b', '${UUID_B}', '${JSON.stringify({ id: 'b', uuid: UUID_B })}'),
('c', '${UUID_C}', '${JSON.stringify(rawConvoC)}');
INSERT INTO messages
(id, json)
VALUES
('m', '${JSON.stringify({
id: 'm',
groupV2Change: {
from: 'a',
details: [
...CHANGE_TYPES.map(type => ({ type, conversationId: 'b' })),
...CHANGE_TYPES_WITH_INVITER.map(type => {
return { type, conversationId: 'c', inviter: 'a' };
}),
],
},
sourceUuid: 'a',
invitedGV2Members: [
{
conversationId: 'b',
addedByUserId: 'c',
},
],
})}'),
('n', '${JSON.stringify({
id: 'n',
groupV2Change: {
from: 'not-found',
details: [],
},
sourceUuid: 'a',
})}');
`
);
updateToVersion(43);
const { members, json: convoJSON } = db
.prepare('SELECT members, json FROM conversations WHERE id = "c"')
.get();
assert.strictEqual(members, `${UUID_A} ${UUID_B}`);
assert.deepStrictEqual(JSON.parse(convoJSON), {
id: 'c',
uuid: UUID_C,
membersV2: [
{ uuid: UUID_A, joinedAtVersion: 1 },
{ uuid: UUID_B, joinedAtVersion: 2 },
],
pendingMembersV2: [
{ uuid: UUID_A, addedByUserId: UUID_B, timestamp: 4 },
{ uuid: UUID_B, addedByUserId: UUID_A, timestamp: 5 },
],
pendingAdminApprovalV2: [
{ uuid: UUID_A, timestamp: 6 },
{ uuid: UUID_B, timestamp: 7 },
],
});
const { json: messageMJSON } = db
.prepare('SELECT json FROM messages WHERE id = "m"')
.get();
assert.deepStrictEqual(JSON.parse(messageMJSON), {
id: 'm',
groupV2Change: {
from: UUID_A,
details: [
...CHANGE_TYPES.map(type => ({ type, uuid: UUID_B })),
...CHANGE_TYPES_WITH_INVITER.map(type => {
return {
type,
uuid: UUID_C,
inviter: UUID_A,
};
}),
],
},
sourceUuid: UUID_A,
invitedGV2Members: [
{
uuid: UUID_B,
addedByUserId: UUID_C,
},
],
});
const { json: messageNJSON } = db
.prepare('SELECT json FROM messages WHERE id = "n"')
.get();
assert.deepStrictEqual(JSON.parse(messageNJSON), {
id: 'n',
groupV2Change: {
details: [],
},
sourceUuid: UUID_A,
});
});
});
}); });

View file

@ -14,8 +14,8 @@ export function init(signalProtocolStore: SignalProtocolStore): void {
); );
conversation.addKeyChange(uuid); conversation.addKeyChange(uuid);
const groups = await window.ConversationController.getAllGroupsInvolvingId( const groups = await window.ConversationController.getAllGroupsInvolvingUuid(
conversation.id uuid
); );
for (const group of groups) { for (const group of groups) {
group.addKeyChange(uuid); group.addKeyChange(uuid);

View file

@ -37,7 +37,7 @@ import type { CallbackResultType, CustomError } from './Types.d';
import { isValidNumber } from '../types/PhoneNumber'; import { isValidNumber } from '../types/PhoneNumber';
import { Address } from '../types/Address'; import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress'; import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID'; import { UUID, isValidUuid } from '../types/UUID';
import { Sessions, IdentityKeys } from '../LibSignalStores'; import { Sessions, IdentityKeys } from '../LibSignalStores';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup'; import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
import { getKeysForIdentifier } from './getKeysForIdentifier'; import { getKeysForIdentifier } from './getKeysForIdentifier';
@ -656,7 +656,7 @@ export default class OutgoingMessage {
async sendToIdentifier(providedIdentifier: string): Promise<void> { async sendToIdentifier(providedIdentifier: string): Promise<void> {
let identifier = providedIdentifier; let identifier = providedIdentifier;
try { try {
if (window.isValidGuid(identifier)) { if (isValidUuid(identifier)) {
// We're good! // We're good!
} else if (isValidNumber(identifier)) { } else if (isValidNumber(identifier)) {
if (!window.textsecure.messaging) { if (!window.textsecure.messaging) {

View file

@ -1,14 +1,21 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { v4 as generateUUID } from 'uuid';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { isValidGuid } from '../util/isValidGuid';
export type UUIDStringType = `${string}-${string}-${string}-${string}-${string}`; export type UUIDStringType = `${string}-${string}-${string}-${string}-${string}`;
export const isValidUuid = (value: unknown): value is UUIDStringType =>
typeof value === 'string' &&
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
value
);
export class UUID { export class UUID {
constructor(protected readonly value: string) { constructor(protected readonly value: string) {
strictAssert(isValidGuid(value), `Invalid UUID: ${value}`); strictAssert(isValidUuid(value), `Invalid UUID: ${value}`);
} }
public toString(): UUIDStringType { public toString(): UUIDStringType {
@ -41,4 +48,24 @@ export class UUID {
); );
return uuid; return uuid;
} }
public static generate(): UUID {
return new UUID(generateUUID());
}
public static cast(value: UUIDStringType): never;
public static cast(value: string): UUIDStringType;
public static cast(value: string): UUIDStringType {
return new UUID(value.toLowerCase()).toString();
}
// For testing
public static fromPrefix(value: string): UUID {
let padded = value;
while (padded.length < 8) {
padded += '0';
}
return new UUID(`${padded}-0000-4000-8000-${'0'.repeat(12)}`);
}
} }

View file

@ -31,3 +31,11 @@ export function strictAssert(
throw new Error(message); throw new Error(message);
} }
} }
/**
* Asserts that the type of value is not a promise.
* (Useful for database modules)
*/
export function assertSync<T, X>(value: T extends Promise<X> ? never : T): T {
return value;
}

View file

@ -3,6 +3,7 @@
import { compact } from 'lodash'; import { compact } from 'lodash';
import type { ConversationAttributesType } from '../model-types.d'; import type { ConversationAttributesType } from '../model-types.d';
import type { UUIDStringType } from '../types/UUID';
import { isDirectConversation } from './whatTypeOfConversation'; import { isDirectConversation } from './whatTypeOfConversation';
export function getConversationMembers( export function getConversationMembers(
@ -15,7 +16,7 @@ export function getConversationMembers(
if (conversationAttrs.membersV2) { if (conversationAttrs.membersV2) {
const { includePendingMembers } = options; const { includePendingMembers } = options;
const members: Array<{ conversationId: string }> = includePendingMembers const members: Array<{ uuid: UUIDStringType }> = includePendingMembers
? [ ? [
...(conversationAttrs.membersV2 || []), ...(conversationAttrs.membersV2 || []),
...(conversationAttrs.pendingMembersV2 || []), ...(conversationAttrs.pendingMembersV2 || []),
@ -24,9 +25,7 @@ export function getConversationMembers(
return compact( return compact(
members.map(member => { members.map(member => {
const conversation = window.ConversationController.get( const conversation = window.ConversationController.get(member.uuid);
member.conversationId
);
// In groups we won't sent to contacts we believe are unregistered // In groups we won't sent to contacts we believe are unregistered
if (conversation && conversation.isUnregistered()) { if (conversation && conversation.isUnregistered()) {

View file

@ -7,6 +7,7 @@ import type {
GroupV2RequestingMembership, GroupV2RequestingMembership,
} from '../components/conversation/conversation-details/PendingInvites'; } from '../components/conversation/conversation-details/PendingInvites';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import type { UUIDStringType } from '../types/UUID';
import { isConversationUnregistered } from './isConversationUnregistered'; import { isConversationUnregistered } from './isConversationUnregistered';
export const getGroupMemberships = ( export const getGroupMemberships = (
@ -20,7 +21,7 @@ export const getGroupMemberships = (
'memberships' | 'pendingApprovalMemberships' | 'pendingMemberships' 'memberships' | 'pendingApprovalMemberships' | 'pendingMemberships'
> >
>, >,
getConversationById: (conversationId: string) => undefined | ConversationType getConversationByUuid: (uuid: UUIDStringType) => undefined | ConversationType
): { ): {
memberships: Array<GroupV2Membership>; memberships: Array<GroupV2Membership>;
pendingApprovalMemberships: Array<GroupV2RequestingMembership>; pendingApprovalMemberships: Array<GroupV2RequestingMembership>;
@ -28,7 +29,7 @@ export const getGroupMemberships = (
} => ({ } => ({
memberships: memberships.reduce( memberships: memberships.reduce(
(result: Array<GroupV2Membership>, membership) => { (result: Array<GroupV2Membership>, membership) => {
const member = getConversationById(membership.conversationId); const member = getConversationByUuid(membership.uuid);
if (!member) { if (!member) {
return result; return result;
} }
@ -38,7 +39,7 @@ export const getGroupMemberships = (
), ),
pendingApprovalMemberships: pendingApprovalMemberships.reduce( pendingApprovalMemberships: pendingApprovalMemberships.reduce(
(result: Array<GroupV2RequestingMembership>, membership) => { (result: Array<GroupV2RequestingMembership>, membership) => {
const member = getConversationById(membership.conversationId); const member = getConversationByUuid(membership.uuid);
if (!member || isConversationUnregistered(member)) { if (!member || isConversationUnregistered(member)) {
return result; return result;
} }
@ -48,7 +49,7 @@ export const getGroupMemberships = (
), ),
pendingMemberships: pendingMemberships.reduce( pendingMemberships: pendingMemberships.reduce(
(result: Array<GroupV2PendingMembership>, membership) => { (result: Array<GroupV2PendingMembership>, membership) => {
const member = getConversationById(membership.conversationId); const member = getConversationByUuid(membership.uuid);
if (!member || isConversationUnregistered(member)) { if (!member || isConversationUnregistered(member)) {
return result; return result;
} }

View file

@ -1,8 +0,0 @@
// Copyright 2017-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export const isValidGuid = (value: unknown): value is string =>
typeof value === 'string' &&
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
value
);

View file

@ -1,12 +1,12 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { isValidUuid } from '../types/UUID';
import { assert } from './assert'; import { assert } from './assert';
import { isValidGuid } from './isValidGuid';
export function normalizeUuid(uuid: string, context: string): string { export function normalizeUuid(uuid: string, context: string): string {
assert( assert(
isValidGuid(uuid), isValidUuid(uuid),
`Normalizing invalid uuid: ${uuid} in context "${context}"` `Normalizing invalid uuid: ${uuid} in context "${context}"`
); );

View file

@ -273,7 +273,7 @@ export async function sendToGroupViaSenderKey(options: {
conversation.set({ conversation.set({
senderKeyInfo: { senderKeyInfo: {
createdAtDate: Date.now(), createdAtDate: Date.now(),
distributionId: window.getGuid(), distributionId: UUID.generate().toString(),
memberDevices: [], memberDevices: [],
}, },
}); });

View file

@ -22,6 +22,8 @@ import {
UuidCiphertext, UuidCiphertext,
} from 'zkgroup'; } from 'zkgroup';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { UUID } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
export * from 'zkgroup'; export * from 'zkgroup';
@ -63,7 +65,7 @@ export function decryptGroupBlob(
export function decryptProfileKeyCredentialPresentation( export function decryptProfileKeyCredentialPresentation(
clientZkGroupCipher: ClientZkGroupCipher, clientZkGroupCipher: ClientZkGroupCipher,
presentationBuffer: Uint8Array presentationBuffer: Uint8Array
): { profileKey: Uint8Array; uuid: string } { ): { profileKey: Uint8Array; uuid: UUIDStringType } {
const presentation = new ProfileKeyCredentialPresentation( const presentation = new ProfileKeyCredentialPresentation(
uint8ArrayToCompatArray(presentationBuffer) uint8ArrayToCompatArray(presentationBuffer)
); );
@ -79,14 +81,14 @@ export function decryptProfileKeyCredentialPresentation(
return { return {
profileKey: compatArrayToUint8Array(profileKey.serialize()), profileKey: compatArrayToUint8Array(profileKey.serialize()),
uuid, uuid: UUID.cast(uuid),
}; };
} }
export function decryptProfileKey( export function decryptProfileKey(
clientZkGroupCipher: ClientZkGroupCipher, clientZkGroupCipher: ClientZkGroupCipher,
profileKeyCiphertextBuffer: Uint8Array, profileKeyCiphertextBuffer: Uint8Array,
uuid: string uuid: UUIDStringType
): Uint8Array { ): Uint8Array {
const profileKeyCiphertext = new ProfileKeyCiphertext( const profileKeyCiphertext = new ProfileKeyCiphertext(
uint8ArrayToCompatArray(profileKeyCiphertextBuffer) uint8ArrayToCompatArray(profileKeyCiphertextBuffer)
@ -113,7 +115,7 @@ export function decryptUuid(
export function deriveProfileKeyVersion( export function deriveProfileKeyVersion(
profileKeyBase64: string, profileKeyBase64: string,
uuid: string uuid: UUIDStringType
): string { ): string {
const profileKeyArray = base64ToCompatArray(profileKeyBase64); const profileKeyArray = base64ToCompatArray(profileKeyBase64);
const profileKey = new ProfileKey(profileKeyArray); const profileKey = new ProfileKey(profileKeyArray);
@ -167,7 +169,7 @@ export function encryptGroupBlob(
export function encryptUuid( export function encryptUuid(
clientZkGroupCipher: ClientZkGroupCipher, clientZkGroupCipher: ClientZkGroupCipher,
uuidPlaintext: string uuidPlaintext: UUIDStringType
): Uint8Array { ): Uint8Array {
const uuidCiphertext = clientZkGroupCipher.encryptUuid(uuidPlaintext); const uuidCiphertext = clientZkGroupCipher.encryptUuid(uuidPlaintext);
@ -176,7 +178,7 @@ export function encryptUuid(
export function generateProfileKeyCredentialRequest( export function generateProfileKeyCredentialRequest(
clientZkProfileCipher: ClientZkProfileOperations, clientZkProfileCipher: ClientZkProfileOperations,
uuid: string, uuid: UUIDStringType,
profileKeyBase64: string profileKeyBase64: string
): { context: ProfileKeyCredentialRequestContext; requestHex: string } { ): { context: ProfileKeyCredentialRequestContext; requestHex: string } {
const profileKeyArray = base64ToCompatArray(profileKeyBase64); const profileKeyArray = base64ToCompatArray(profileKeyBase64);
@ -287,7 +289,7 @@ export function handleProfileKeyCredential(
export function deriveProfileKeyCommitment( export function deriveProfileKeyCommitment(
profileKeyBase64: string, profileKeyBase64: string,
uuid: string uuid: UUIDStringType
): string { ): string {
const profileKeyArray = base64ToCompatArray(profileKeyBase64); const profileKeyArray = base64ToCompatArray(profileKeyBase64);
const profileKey = new ProfileKey(profileKeyArray); const profileKey = new ProfileKey(profileKeyArray);

View file

@ -1258,7 +1258,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}); });
const invitedMemberIds = pendingMembersV2.map( const invitedMemberIds = pendingMembersV2.map(
(item: GroupV2PendingMemberType) => item.conversationId (item: GroupV2PendingMemberType) => item.uuid
); );
this.migrationDialog = new Whisper.ReactWrapperView({ this.migrationDialog = new Whisper.ReactWrapperView({

3
ts/window.d.ts vendored
View file

@ -102,7 +102,6 @@ import { SocketStatus } from './types/SocketStatus';
import SyncRequest from './textsecure/SyncRequest'; import SyncRequest from './textsecure/SyncRequest';
import { ConversationColorType, CustomColorType } from './types/Colors'; import { ConversationColorType, CustomColorType } from './types/Colors';
import { MessageController } from './util/MessageController'; import { MessageController } from './util/MessageController';
import { isValidGuid } from './util/isValidGuid';
import { StateType } from './state/reducer'; import { StateType } from './state/reducer';
import { SystemTraySetting } from './types/SystemTraySetting'; import { SystemTraySetting } from './types/SystemTraySetting';
import { UUID } from './types/UUID'; import { UUID } from './types/UUID';
@ -199,7 +198,6 @@ declare global {
getBuildCreation: () => number; getBuildCreation: () => number;
getEnvironment: typeof getEnvironment; getEnvironment: typeof getEnvironment;
getExpiration: () => string; getExpiration: () => string;
getGuid: () => string;
getHostName: () => string; getHostName: () => string;
getInboxCollection: () => ConversationModelCollectionType; getInboxCollection: () => ConversationModelCollectionType;
getInteractionMode: () => 'mouse' | 'keyboard'; getInteractionMode: () => 'mouse' | 'keyboard';
@ -219,7 +217,6 @@ declare global {
isAfterVersion: (version: string, anotherVersion: string) => boolean; isAfterVersion: (version: string, anotherVersion: string) => boolean;
isBeforeVersion: (version: string, anotherVersion: string) => boolean; isBeforeVersion: (version: string, anotherVersion: string) => boolean;
isFullScreen: () => boolean; isFullScreen: () => boolean;
isValidGuid: typeof isValidGuid;
libphonenumber: { libphonenumber: {
util: { util: {
getRegionCodeForNumber: (number: string) => string; getRegionCodeForNumber: (number: string) => string;