signal-desktop/ts/services/storageRecordOps.ts

1559 lines
47 KiB
TypeScript
Raw Normal View History

// Copyright 2020-2022 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2021-04-06 22:54:47 +00:00
import { isEqual, isNumber } from 'lodash';
2021-07-13 18:54:53 +00:00
import Long from 'long';
2020-09-09 00:56:23 +00:00
2022-07-07 00:34:13 +00:00
import {
uuidToBytes,
bytesToUuid,
deriveMasterKeyFromGroupV1,
} from '../Crypto';
2021-06-22 14:46:42 +00:00
import * as Bytes from '../Bytes';
2020-11-20 17:30:45 +00:00
import {
deriveGroupFields,
waitThenMaybeUpdateGroup,
waitThenRespondToGroupV2Migration,
} from '../groups';
import { assertDev } from '../util/assert';
2022-03-02 22:53:47 +00:00
import { dropNull } from '../util/dropNull';
2021-07-09 19:36:10 +00:00
import { normalizeUuid } from '../util/normalizeUuid';
import { missingCaseError } from '../util/missingCaseError';
import {
PhoneNumberSharingMode,
parsePhoneNumberSharingMode,
} from '../util/phoneNumberSharingMode';
import {
PhoneNumberDiscoverability,
parsePhoneNumberDiscoverability,
} from '../util/phoneNumberDiscoverability';
2021-04-08 19:27:20 +00:00
import { arePinnedConversationsEqual } from '../util/arePinnedConversationsEqual';
import type { ConversationModel } from '../models/conversations';
2021-04-09 16:19:38 +00:00
import {
getSafeLongFromTimestamp,
getTimestampFromLong,
} from '../util/timestampLongUtils';
2021-06-01 20:45:43 +00:00
import {
get as getUniversalExpireTimer,
set as setUniversalExpireTimer,
} from '../util/universalExpireTimer';
import { ourProfileKeyService } from './ourProfileKey';
import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation';
import { isValidUuid, UUID, UUIDKind } from '../types/UUID';
import * as preferredReactionEmoji from '../reactions/preferredReactionEmoji';
2021-07-13 18:54:53 +00:00
import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log';
2022-07-01 00:52:03 +00:00
import type { UUIDStringType } from '../types/UUID';
2022-08-03 17:10:49 +00:00
import * as Stickers from '../types/Stickers';
import type {
StoryDistributionWithMembersType,
StickerPackInfoType,
} from '../sql/Interface';
2022-07-01 00:52:03 +00:00
import dataInterface from '../sql/Client';
import { MY_STORIES_ID } from '../types/Stories';
import * as RemoteConfig from '../RemoteConfig';
const MY_STORIES_BYTES = uuidToBytes(MY_STORIES_ID);
2020-09-09 00:56:23 +00:00
2020-09-09 02:25:05 +00:00
type RecordClass =
2021-07-13 18:54:53 +00:00
| Proto.IAccountRecord
| Proto.IContactRecord
| Proto.IGroupV1Record
| Proto.IGroupV2Record;
2020-09-09 00:56:23 +00:00
2022-02-08 18:00:18 +00:00
export type MergeResultType = Readonly<{
hasConflict: boolean;
2022-02-11 21:05:24 +00:00
shouldDrop?: boolean;
2022-02-08 18:00:18 +00:00
conversation?: ConversationModel;
2022-03-09 18:22:34 +00:00
needsProfileFetch?: boolean;
updatedConversations?: ReadonlyArray<ConversationModel>;
2022-02-08 18:00:18 +00:00
oldStorageID?: string;
oldStorageVersion?: number;
details: ReadonlyArray<string>;
}>;
type HasConflictResultType = Readonly<{
hasConflict: boolean;
details: ReadonlyArray<string>;
}>;
2021-07-13 18:54:53 +00:00
function toRecordVerified(verified: number): Proto.ContactRecord.IdentityState {
2020-09-09 00:56:23 +00:00
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
2021-07-13 18:54:53 +00:00
const STATE_ENUM = Proto.ContactRecord.IdentityState;
2020-09-09 00:56:23 +00:00
switch (verified) {
case VERIFIED_ENUM.VERIFIED:
return STATE_ENUM.VERIFIED;
case VERIFIED_ENUM.UNVERIFIED:
return STATE_ENUM.UNVERIFIED;
default:
return STATE_ENUM.DEFAULT;
}
}
function addUnknownFields(
record: RecordClass,
2022-02-08 18:00:18 +00:00
conversation: ConversationModel,
details: Array<string>
2020-09-09 00:56:23 +00:00
): void {
if (record.__unknownFields) {
2022-02-08 18:00:18 +00:00
details.push('adding unknown fields');
2020-09-09 00:56:23 +00:00
conversation.set({
2021-07-13 18:54:53 +00:00
storageUnknownFields: Bytes.toBase64(
Bytes.concatenate(record.__unknownFields)
),
2020-09-09 00:56:23 +00:00
});
2020-10-06 22:25:00 +00:00
} else if (conversation.get('storageUnknownFields')) {
// If the record doesn't have unknown fields attached but we have them
// saved locally then we need to clear it out
2022-02-08 18:00:18 +00:00
details.push('clearing unknown fields');
2020-10-06 22:25:00 +00:00
conversation.unset('storageUnknownFields');
2020-09-09 00:56:23 +00:00
}
}
function applyUnknownFields(
record: RecordClass,
conversation: ConversationModel
2020-09-09 00:56:23 +00:00
): void {
const storageUnknownFields = conversation.get('storageUnknownFields');
if (storageUnknownFields) {
log.info(
2020-10-07 23:44:55 +00:00
'storageService.applyUnknownFields: Applying unknown fields for',
2022-02-08 18:00:18 +00:00
conversation.idForLogging()
2020-09-16 18:04:28 +00:00
);
2020-09-09 00:56:23 +00:00
// eslint-disable-next-line no-param-reassign
2021-07-13 18:54:53 +00:00
record.__unknownFields = [Bytes.fromBase64(storageUnknownFields)];
2020-09-09 00:56:23 +00:00
}
}
export async function toContactRecord(
conversation: ConversationModel
2021-07-13 18:54:53 +00:00
): Promise<Proto.ContactRecord> {
const contactRecord = new Proto.ContactRecord();
2021-10-26 22:59:08 +00:00
const uuid = conversation.getUuid();
2021-07-13 18:54:53 +00:00
if (uuid) {
2021-10-26 22:59:08 +00:00
contactRecord.serviceUuid = uuid.toString();
2020-09-09 00:56:23 +00:00
}
2021-07-13 18:54:53 +00:00
const e164 = conversation.get('e164');
if (e164) {
contactRecord.serviceE164 = e164;
2020-09-09 00:56:23 +00:00
}
2022-08-10 18:39:04 +00:00
const pni = conversation.get('pni');
2022-08-17 22:06:17 +00:00
if (pni && RemoteConfig.isEnabled('desktop.pnp')) {
2022-08-10 18:39:04 +00:00
contactRecord.pni = pni;
}
2021-07-13 18:54:53 +00:00
const profileKey = conversation.get('profileKey');
if (profileKey) {
contactRecord.profileKey = Bytes.fromBase64(String(profileKey));
2020-09-09 00:56:23 +00:00
}
2021-10-26 22:59:08 +00:00
const identityKey = uuid
? await window.textsecure.storage.protocol.loadIdentityKey(uuid)
: undefined;
2020-09-09 00:56:23 +00:00
if (identityKey) {
2021-09-24 00:49:05 +00:00
contactRecord.identityKey = identityKey;
2020-09-09 00:56:23 +00:00
}
2021-07-13 18:54:53 +00:00
const verified = conversation.get('verified');
if (verified) {
contactRecord.identityState = toRecordVerified(Number(verified));
2020-09-09 00:56:23 +00:00
}
2021-07-13 18:54:53 +00:00
const profileName = conversation.get('profileName');
if (profileName) {
contactRecord.givenName = profileName;
2020-09-09 00:56:23 +00:00
}
2021-07-13 18:54:53 +00:00
const profileFamilyName = conversation.get('profileFamilyName');
if (profileFamilyName) {
contactRecord.familyName = profileFamilyName;
2020-09-09 00:56:23 +00:00
}
contactRecord.blocked = conversation.isBlocked();
contactRecord.whitelisted = Boolean(conversation.get('profileSharing'));
contactRecord.archived = Boolean(conversation.get('isArchived'));
contactRecord.markedUnread = Boolean(conversation.get('markedUnread'));
2021-04-09 16:19:38 +00:00
contactRecord.mutedUntilTimestamp = getSafeLongFromTimestamp(
conversation.get('muteExpiresAt')
);
2022-03-04 21:14:52 +00:00
if (conversation.get('hideStory') !== undefined) {
contactRecord.hideStory = Boolean(conversation.get('hideStory'));
}
contactRecord.unregisteredAtTimestamp = getSafeLongFromTimestamp(
conversation.get('firstUnregisteredAt')
);
2020-09-09 00:56:23 +00:00
applyUnknownFields(contactRecord, conversation);
return contactRecord;
}
2022-03-04 21:14:52 +00:00
export function toAccountRecord(
conversation: ConversationModel
2022-03-04 21:14:52 +00:00
): Proto.AccountRecord {
2021-07-13 18:54:53 +00:00
const accountRecord = new Proto.AccountRecord();
2020-09-09 00:56:23 +00:00
if (conversation.get('profileKey')) {
2021-07-13 18:54:53 +00:00
accountRecord.profileKey = Bytes.fromBase64(
2020-09-09 00:56:23 +00:00
String(conversation.get('profileKey'))
);
}
if (conversation.get('profileName')) {
accountRecord.givenName = conversation.get('profileName') || '';
}
if (conversation.get('profileFamilyName')) {
accountRecord.familyName = conversation.get('profileFamilyName') || '';
}
const avatarUrl = window.storage.get('avatarUrl');
if (avatarUrl !== undefined) {
accountRecord.avatarUrl = avatarUrl;
}
2020-09-09 00:56:23 +00:00
accountRecord.noteToSelfArchived = Boolean(conversation.get('isArchived'));
accountRecord.noteToSelfMarkedUnread = Boolean(
conversation.get('markedUnread')
);
2021-08-18 20:08:14 +00:00
accountRecord.readReceipts = Boolean(window.Events.getReadReceiptSetting());
2020-09-09 00:56:23 +00:00
accountRecord.sealedSenderIndicators = Boolean(
window.storage.get('sealedSenderIndicators')
);
accountRecord.typingIndicators = Boolean(
2021-08-18 20:08:14 +00:00
window.Events.getTypingIndicatorSetting()
2020-09-09 00:56:23 +00:00
);
2021-08-18 20:08:14 +00:00
accountRecord.linkPreviews = Boolean(window.Events.getLinkPreviewSetting());
const preferContactAvatars = window.storage.get('preferContactAvatars');
if (preferContactAvatars !== undefined) {
accountRecord.preferContactAvatars = Boolean(preferContactAvatars);
}
const primarySendsSms = window.storage.get('primarySendsSms');
if (primarySendsSms !== undefined) {
accountRecord.primarySendsSms = Boolean(primarySendsSms);
}
2021-08-05 23:34:49 +00:00
const accountE164 = window.storage.get('accountE164');
if (accountE164 !== undefined) {
accountRecord.e164 = accountE164;
}
const rawPreferredReactionEmoji = window.storage.get(
'preferredReactionEmoji'
);
if (preferredReactionEmoji.canBeSynced(rawPreferredReactionEmoji)) {
accountRecord.preferredReactionEmoji = rawPreferredReactionEmoji;
}
2021-06-01 20:45:43 +00:00
const universalExpireTimer = getUniversalExpireTimer();
if (universalExpireTimer) {
accountRecord.universalExpireTimer = Number(universalExpireTimer);
}
const PHONE_NUMBER_SHARING_MODE_ENUM =
2021-07-13 18:54:53 +00:00
Proto.AccountRecord.PhoneNumberSharingMode;
const phoneNumberSharingMode = parsePhoneNumberSharingMode(
window.storage.get('phoneNumberSharingMode')
);
switch (phoneNumberSharingMode) {
case PhoneNumberSharingMode.Everybody:
accountRecord.phoneNumberSharingMode =
PHONE_NUMBER_SHARING_MODE_ENUM.EVERYBODY;
break;
case PhoneNumberSharingMode.ContactsOnly:
accountRecord.phoneNumberSharingMode =
PHONE_NUMBER_SHARING_MODE_ENUM.CONTACTS_ONLY;
break;
case PhoneNumberSharingMode.Nobody:
accountRecord.phoneNumberSharingMode =
PHONE_NUMBER_SHARING_MODE_ENUM.NOBODY;
break;
default:
throw missingCaseError(phoneNumberSharingMode);
}
const phoneNumberDiscoverability = parsePhoneNumberDiscoverability(
window.storage.get('phoneNumberDiscoverability')
);
switch (phoneNumberDiscoverability) {
case PhoneNumberDiscoverability.Discoverable:
accountRecord.notDiscoverableByPhoneNumber = false;
break;
case PhoneNumberDiscoverability.NotDiscoverable:
accountRecord.notDiscoverableByPhoneNumber = true;
break;
default:
throw missingCaseError(phoneNumberDiscoverability);
}
2020-10-10 14:25:17 +00:00
const pinnedConversations = window.storage
.get('pinnedConversationIds', new Array<string>())
2020-10-02 18:30:43 +00:00
.map(id => {
const pinnedConversation = window.ConversationController.get(id);
if (pinnedConversation) {
2021-11-11 22:43:05 +00:00
const pinnedConversationRecord =
new Proto.AccountRecord.PinnedConversation();
2020-10-02 18:30:43 +00:00
if (pinnedConversation.get('type') === 'private') {
pinnedConversationRecord.identifier = 'contact';
pinnedConversationRecord.contact = {
uuid: pinnedConversation.get('uuid'),
e164: pinnedConversation.get('e164'),
};
} else if (isGroupV1(pinnedConversation.attributes)) {
2020-10-02 18:30:43 +00:00
pinnedConversationRecord.identifier = 'legacyGroupId';
const groupId = pinnedConversation.get('groupId');
if (!groupId) {
throw new Error(
'toAccountRecord: trying to pin a v1 Group without groupId'
);
}
2021-07-13 18:54:53 +00:00
pinnedConversationRecord.legacyGroupId = Bytes.fromBinary(groupId);
} else if (isGroupV2(pinnedConversation.attributes)) {
2020-10-02 18:30:43 +00:00
pinnedConversationRecord.identifier = 'groupMasterKey';
const masterKey = pinnedConversation.get('masterKey');
if (!masterKey) {
throw new Error(
'toAccountRecord: trying to pin a v2 Group without masterKey'
);
}
2021-07-13 18:54:53 +00:00
pinnedConversationRecord.groupMasterKey = Bytes.fromBase64(masterKey);
2020-10-02 18:30:43 +00:00
}
return pinnedConversationRecord;
}
return undefined;
})
.filter(
(
pinnedConversationClass
2021-07-13 18:54:53 +00:00
): pinnedConversationClass is Proto.AccountRecord.PinnedConversation =>
2020-10-02 18:30:43 +00:00
pinnedConversationClass !== undefined
);
2020-09-09 00:56:23 +00:00
2020-10-10 14:25:17 +00:00
accountRecord.pinnedConversations = pinnedConversations;
const subscriberId = window.storage.get('subscriberId');
if (subscriberId instanceof Uint8Array) {
accountRecord.subscriberId = subscriberId;
}
const subscriberCurrencyCode = window.storage.get('subscriberCurrencyCode');
if (typeof subscriberCurrencyCode === 'string') {
accountRecord.subscriberCurrencyCode = subscriberCurrencyCode;
}
2022-05-25 20:44:05 +00:00
const displayBadgesOnProfile = window.storage.get('displayBadgesOnProfile');
if (displayBadgesOnProfile !== undefined) {
accountRecord.displayBadgesOnProfile = displayBadgesOnProfile;
}
const keepMutedChatsArchived = window.storage.get('keepMutedChatsArchived');
if (keepMutedChatsArchived !== undefined) {
accountRecord.keepMutedChatsArchived = keepMutedChatsArchived;
}
const hasSetMyStoriesPrivacy = window.storage.get('hasSetMyStoriesPrivacy');
if (hasSetMyStoriesPrivacy !== undefined) {
accountRecord.hasSetMyStoriesPrivacy = hasSetMyStoriesPrivacy;
}
2020-09-09 00:56:23 +00:00
applyUnknownFields(accountRecord, conversation);
return accountRecord;
}
2022-03-04 21:14:52 +00:00
export function toGroupV1Record(
conversation: ConversationModel
2022-03-04 21:14:52 +00:00
): Proto.GroupV1Record {
2021-07-13 18:54:53 +00:00
const groupV1Record = new Proto.GroupV1Record();
2020-09-09 00:56:23 +00:00
2021-07-13 18:54:53 +00:00
groupV1Record.id = Bytes.fromBinary(String(conversation.get('groupId')));
2020-09-09 00:56:23 +00:00
groupV1Record.blocked = conversation.isBlocked();
groupV1Record.whitelisted = Boolean(conversation.get('profileSharing'));
groupV1Record.archived = Boolean(conversation.get('isArchived'));
groupV1Record.markedUnread = Boolean(conversation.get('markedUnread'));
2021-04-09 16:19:38 +00:00
groupV1Record.mutedUntilTimestamp = getSafeLongFromTimestamp(
conversation.get('muteExpiresAt')
);
2020-09-09 00:56:23 +00:00
applyUnknownFields(groupV1Record, conversation);
return groupV1Record;
}
2022-03-04 21:14:52 +00:00
export function toGroupV2Record(
conversation: ConversationModel
2022-03-04 21:14:52 +00:00
): Proto.GroupV2Record {
2021-07-13 18:54:53 +00:00
const groupV2Record = new Proto.GroupV2Record();
2020-09-09 02:25:05 +00:00
const masterKey = conversation.get('masterKey');
if (masterKey !== undefined) {
2021-07-13 18:54:53 +00:00
groupV2Record.masterKey = Bytes.fromBase64(masterKey);
2020-09-09 02:25:05 +00:00
}
groupV2Record.blocked = conversation.isBlocked();
groupV2Record.whitelisted = Boolean(conversation.get('profileSharing'));
groupV2Record.archived = Boolean(conversation.get('isArchived'));
groupV2Record.markedUnread = Boolean(conversation.get('markedUnread'));
2021-04-09 16:19:38 +00:00
groupV2Record.mutedUntilTimestamp = getSafeLongFromTimestamp(
conversation.get('muteExpiresAt')
);
2021-08-05 12:35:33 +00:00
groupV2Record.dontNotifyForMentionsIfMuted = Boolean(
conversation.get('dontNotifyForMentionsIfMuted')
);
2022-03-04 21:14:52 +00:00
groupV2Record.hideStory = Boolean(conversation.get('hideStory'));
2020-09-09 02:25:05 +00:00
applyUnknownFields(groupV2Record, conversation);
return groupV2Record;
}
2022-07-01 00:52:03 +00:00
export function toStoryDistributionListRecord(
storyDistributionList: StoryDistributionWithMembersType
): Proto.StoryDistributionListRecord {
const storyDistributionListRecord = new Proto.StoryDistributionListRecord();
2022-07-07 00:34:13 +00:00
storyDistributionListRecord.identifier = uuidToBytes(
2022-07-01 00:52:03 +00:00
storyDistributionList.id
);
storyDistributionListRecord.name = storyDistributionList.name;
storyDistributionListRecord.deletedAtTimestamp = getSafeLongFromTimestamp(
storyDistributionList.deletedAtTimestamp
);
storyDistributionListRecord.allowsReplies = Boolean(
storyDistributionList.allowsReplies
);
storyDistributionListRecord.isBlockList = Boolean(
storyDistributionList.isBlockList
);
storyDistributionListRecord.recipientUuids = storyDistributionList.members;
if (storyDistributionList.storageUnknownFields) {
storyDistributionListRecord.__unknownFields = [
storyDistributionList.storageUnknownFields,
];
}
return storyDistributionListRecord;
}
2022-08-03 17:10:49 +00:00
export function toStickerPackRecord(
stickerPack: StickerPackInfoType
): Proto.StickerPackRecord {
const stickerPackRecord = new Proto.StickerPackRecord();
stickerPackRecord.packId = Bytes.fromHex(stickerPack.id);
if (stickerPack.uninstalledAt !== undefined) {
stickerPackRecord.deletedAtTimestamp = Long.fromNumber(
stickerPack.uninstalledAt
);
} else {
stickerPackRecord.packKey = Bytes.fromBase64(stickerPack.key);
if (stickerPack.position) {
stickerPackRecord.position = stickerPack.position;
}
}
if (stickerPack.storageUnknownFields) {
stickerPackRecord.__unknownFields = [stickerPack.storageUnknownFields];
}
return stickerPackRecord;
}
2021-07-13 18:54:53 +00:00
type MessageRequestCapableRecord = Proto.IContactRecord | Proto.IGroupV1Record;
2020-09-09 00:56:23 +00:00
function applyMessageRequestState(
record: MessageRequestCapableRecord,
conversation: ConversationModel
2020-09-09 00:56:23 +00:00
): void {
2021-07-13 18:54:53 +00:00
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
2020-10-06 17:06:34 +00:00
2020-09-09 00:56:23 +00:00
if (record.blocked) {
2020-10-06 17:06:34 +00:00
conversation.applyMessageRequestResponse(messageRequestEnum.BLOCK, {
fromSync: true,
viaStorageServiceSync: true,
});
2020-09-09 00:56:23 +00:00
} else if (record.whitelisted) {
// unblocking is also handled by this function which is why the next
// condition is part of the else-if and not separate
2020-10-06 17:06:34 +00:00
conversation.applyMessageRequestResponse(messageRequestEnum.ACCEPT, {
fromSync: true,
viaStorageServiceSync: true,
});
2020-09-09 00:56:23 +00:00
} else if (!record.blocked) {
// if the condition above failed the state could still be blocked=false
// in which case we should unblock the conversation
conversation.unblock({ viaStorageServiceSync: true });
}
if (record.whitelisted === false) {
2020-09-09 00:56:23 +00:00
conversation.disableProfileSharing({ viaStorageServiceSync: true });
}
}
2020-09-16 18:04:28 +00:00
type RecordClassObject = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2020-09-16 18:04:28 +00:00
[key: string]: any;
};
function doRecordsConflict(
localRecord: RecordClassObject,
2022-02-08 18:00:18 +00:00
remoteRecord: RecordClassObject
): HasConflictResultType {
const details = new Array<string>();
2020-09-16 18:04:28 +00:00
2022-02-08 18:00:18 +00:00
for (const key of Object.keys(remoteRecord)) {
2020-09-16 18:04:28 +00:00
const localValue = localRecord[key];
const remoteValue = remoteRecord[key];
2021-09-24 00:49:05 +00:00
// Sometimes we have a ByteBuffer and an Uint8Array, this ensures that we
2021-04-08 19:27:20 +00:00
// are comparing them both equally by converting them into base64 string.
2021-07-13 18:54:53 +00:00
if (localValue instanceof Uint8Array) {
const areEqual = Bytes.areEqual(localValue, remoteValue);
2021-04-08 19:27:20 +00:00
if (!areEqual) {
2022-02-08 18:00:18 +00:00
details.push(`key=${key}: different bytes`);
2021-04-08 19:27:20 +00:00
}
2022-02-08 18:00:18 +00:00
continue;
2021-04-08 19:27:20 +00:00
}
// If both types are Long we can use Long's equals to compare them
2021-11-08 21:43:37 +00:00
if (Long.isLong(localValue) || typeof localValue === 'number') {
if (!Long.isLong(remoteValue) && typeof remoteValue !== 'number') {
2022-02-08 18:00:18 +00:00
details.push(`key=${key}: type mismatch`);
continue;
2021-07-13 18:54:53 +00:00
}
const areEqual = Long.fromValue(localValue).equals(
Long.fromValue(remoteValue)
);
2021-04-08 19:27:20 +00:00
if (!areEqual) {
2022-02-08 18:00:18 +00:00
details.push(`key=${key}: different integers`);
2021-04-08 19:27:20 +00:00
}
2022-02-08 18:00:18 +00:00
continue;
2021-04-08 19:27:20 +00:00
}
if (key === 'pinnedConversations') {
const areEqual = arePinnedConversationsEqual(localValue, remoteValue);
if (!areEqual) {
2022-02-08 18:00:18 +00:00
details.push('pinnedConversations');
2021-04-08 19:27:20 +00:00
}
2022-02-08 18:00:18 +00:00
continue;
2021-04-08 19:27:20 +00:00
}
2020-09-16 18:04:28 +00:00
if (localValue === remoteValue) {
2022-02-08 18:00:18 +00:00
continue;
2020-09-16 18:04:28 +00:00
}
// Sometimes we get `null` values from Protobuf and they should default to
// false, empty string, or 0 for these records we do not count them as
// conflicting.
if (
// eslint-disable-next-line eqeqeq
2020-09-16 18:04:28 +00:00
remoteValue === null &&
2021-04-08 19:27:20 +00:00
(localValue === false ||
localValue === '' ||
localValue === 0 ||
2021-07-13 18:54:53 +00:00
(Long.isLong(localValue) && localValue.toNumber() === 0))
2020-09-16 18:04:28 +00:00
) {
2022-02-08 18:00:18 +00:00
continue;
2020-09-16 18:04:28 +00:00
}
2021-04-06 22:54:47 +00:00
const areEqual = isEqual(localValue, remoteValue);
if (!areEqual) {
2022-02-08 18:00:18 +00:00
details.push(`key=${key}: different values`);
2021-04-06 22:54:47 +00:00
}
2022-02-08 18:00:18 +00:00
}
2021-04-06 22:54:47 +00:00
2022-02-08 18:00:18 +00:00
return {
hasConflict: details.length > 0,
details,
};
2020-09-16 18:04:28 +00:00
}
2020-09-09 00:56:23 +00:00
function doesRecordHavePendingChanges(
mergedRecord: RecordClass,
serviceRecord: RecordClass,
conversation: ConversationModel
2022-02-08 18:00:18 +00:00
): HasConflictResultType {
2020-09-09 00:56:23 +00:00
const shouldSync = Boolean(conversation.get('needsStorageServiceSync'));
2020-09-16 18:04:28 +00:00
if (!shouldSync) {
2022-02-08 18:00:18 +00:00
return { hasConflict: false, details: [] };
2020-09-16 18:04:28 +00:00
}
2022-02-08 18:00:18 +00:00
const { hasConflict, details } = doRecordsConflict(
2020-09-16 18:04:28 +00:00
mergedRecord,
2022-02-08 18:00:18 +00:00
serviceRecord
2020-09-16 18:04:28 +00:00
);
2020-09-09 00:56:23 +00:00
2020-09-16 18:04:28 +00:00
if (!hasConflict) {
2020-09-09 00:56:23 +00:00
conversation.set({ needsStorageServiceSync: false });
}
2022-02-08 18:00:18 +00:00
return {
hasConflict,
details,
};
2020-09-09 00:56:23 +00:00
}
export async function mergeGroupV1Record(
storageID: string,
2022-02-08 18:00:18 +00:00
storageVersion: number,
2021-07-13 18:54:53 +00:00
groupV1Record: Proto.IGroupV1Record
2022-02-08 18:00:18 +00:00
): Promise<MergeResultType> {
2020-09-09 00:56:23 +00:00
if (!groupV1Record.id) {
throw new Error(`No ID for ${storageID}`);
2020-09-09 00:56:23 +00:00
}
2021-07-13 18:54:53 +00:00
const groupId = Bytes.toBinary(groupV1Record.id);
2022-02-08 18:00:18 +00:00
let details = new Array<string>();
2020-09-09 00:56:23 +00:00
// Attempt to fetch an existing group pertaining to the `groupId` or create
// a new group and populate it with the attributes from the record.
let conversation = window.ConversationController.get(groupId);
// Because ConversationController.get retrieves all types of records we
// may sometimes have a situation where we get a record of groupv1 type
// where the binary representation of its ID matches a v2 record in memory.
// Here we ensure that the record we're about to process is GV1 otherwise
// we drop the update.
if (conversation && !isGroupV1(conversation.attributes)) {
2021-04-06 22:54:47 +00:00
throw new Error(
`Record has group type mismatch ${conversation.idForLogging()}`
);
}
2020-09-09 00:56:23 +00:00
if (!conversation) {
// It's possible this group was migrated to a GV2 if so we attempt to
// retrieve the master key and find the conversation locally. If we
// are successful then we continue setting and applying state.
2021-09-24 00:49:05 +00:00
const masterKeyBuffer = deriveMasterKeyFromGroupV1(groupV1Record.id);
const fields = deriveGroupFields(masterKeyBuffer);
2021-06-22 14:46:42 +00:00
const derivedGroupV2Id = Bytes.toBase64(fields.id);
2022-02-08 18:00:18 +00:00
details.push(
'failed to find group by v1 id ' +
`attempting lookup by v2 groupv2(${derivedGroupV2Id})`
);
conversation = window.ConversationController.get(derivedGroupV2Id);
}
2022-02-08 18:00:18 +00:00
if (!conversation) {
2021-04-09 20:12:05 +00:00
if (groupV1Record.id.byteLength !== 16) {
throw new Error('Not a valid gv1');
}
conversation = await window.ConversationController.getOrCreateAndWait(
groupId,
'group'
);
2022-02-08 18:00:18 +00:00
details.push('created a new group locally');
2020-09-09 00:56:23 +00:00
}
2022-02-08 18:00:18 +00:00
const oldStorageID = conversation.get('storageID');
const oldStorageVersion = conversation.get('storageVersion');
2022-02-11 21:05:24 +00:00
if (!isGroupV1(conversation.attributes)) {
details.push('GV1 record for GV2 group, dropping');
return {
// Note: conflicts cause immediate uploads, but we should upload
// only in response to user's action.
hasConflict: false,
2022-02-11 21:05:24 +00:00
shouldDrop: true,
conversation,
oldStorageID,
oldStorageVersion,
details,
};
}
2020-09-09 00:56:23 +00:00
conversation.set({
isArchived: Boolean(groupV1Record.archived),
markedUnread: Boolean(groupV1Record.markedUnread),
2020-09-09 00:56:23 +00:00
storageID,
2022-02-08 18:00:18 +00:00
storageVersion,
2020-09-09 00:56:23 +00:00
});
2021-04-09 16:19:38 +00:00
conversation.setMuteExpiration(
getTimestampFromLong(groupV1Record.mutedUntilTimestamp),
{
viaStorageServiceSync: true,
}
);
2020-09-09 00:56:23 +00:00
applyMessageRequestState(groupV1Record, conversation);
let hasPendingChanges: boolean;
2020-09-09 00:56:23 +00:00
if (isGroupV1(conversation.attributes)) {
2022-02-08 18:00:18 +00:00
addUnknownFields(groupV1Record, conversation, details);
2022-02-08 18:00:18 +00:00
const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(
2022-03-04 21:14:52 +00:00
toGroupV1Record(conversation),
groupV1Record,
conversation
);
2022-02-08 18:00:18 +00:00
details = details.concat(extraDetails);
hasPendingChanges = hasConflict;
} else {
// We cannot preserve unknown fields if local group is V2 and the remote is
// still V1, because the storageItem that we'll put into manifest will have
// a different record type.
// We want to upgrade group in the storage after merging it.
hasPendingChanges = true;
2022-02-08 18:00:18 +00:00
details.push('marking v1 group for an update to v2');
}
2020-09-09 00:56:23 +00:00
2022-02-08 18:00:18 +00:00
return {
hasConflict: hasPendingChanges,
conversation,
oldStorageID,
oldStorageVersion,
details,
2022-03-09 18:22:34 +00:00
updatedConversations: [conversation],
2022-02-08 18:00:18 +00:00
};
2020-09-09 00:56:23 +00:00
}
2022-03-09 18:22:34 +00:00
function getGroupV2Conversation(
2021-07-13 18:54:53 +00:00
masterKeyBuffer: Uint8Array
2022-03-09 18:22:34 +00:00
): ConversationModel {
2021-07-13 18:54:53 +00:00
const groupFields = deriveGroupFields(masterKeyBuffer);
2020-09-09 02:25:05 +00:00
2021-06-22 14:46:42 +00:00
const groupId = Bytes.toBase64(groupFields.id);
2021-07-13 18:54:53 +00:00
const masterKey = Bytes.toBase64(masterKeyBuffer);
2021-06-22 14:46:42 +00:00
const secretParams = Bytes.toBase64(groupFields.secretParams);
const publicParams = Bytes.toBase64(groupFields.publicParams);
2020-09-09 02:25:05 +00:00
2020-11-20 17:30:45 +00:00
// First we check for an existing GroupV2 group
const groupV2 = window.ConversationController.get(groupId);
if (groupV2) {
2022-03-09 18:22:34 +00:00
groupV2.maybeRepairGroupV2({
2020-11-20 17:30:45 +00:00
masterKey,
secretParams,
publicParams,
});
return groupV2;
}
// Then check for V1 group with matching derived GV2 id
const groupV1 = window.ConversationController.getByDerivedGroupV2Id(groupId);
if (groupV1) {
return groupV1;
}
2020-09-09 02:25:05 +00:00
const conversationId = window.ConversationController.ensureGroup(groupId, {
2020-10-06 17:06:34 +00:00
// Note: We don't set active_at, because we don't want the group to show until
// we have information about it beyond these initial details.
// see maybeUpdateGroup().
2020-09-09 02:25:05 +00:00
groupVersion: 2,
masterKey,
secretParams,
publicParams,
});
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
2020-11-20 17:30:45 +00:00
throw new Error(
`getGroupV2Conversation: Failed to create conversation for groupv2(${groupId})`
);
2020-09-09 02:25:05 +00:00
}
2020-11-20 17:30:45 +00:00
return conversation;
}
export async function mergeGroupV2Record(
storageID: string,
2022-02-08 18:00:18 +00:00
storageVersion: number,
2021-07-13 18:54:53 +00:00
groupV2Record: Proto.IGroupV2Record
2022-02-08 18:00:18 +00:00
): Promise<MergeResultType> {
2020-11-20 17:30:45 +00:00
if (!groupV2Record.masterKey) {
throw new Error(`No master key for ${storageID}`);
}
2021-07-13 18:54:53 +00:00
const masterKeyBuffer = groupV2Record.masterKey;
2022-03-09 18:22:34 +00:00
const conversation = getGroupV2Conversation(masterKeyBuffer);
2020-09-09 02:25:05 +00:00
2022-02-08 18:00:18 +00:00
const oldStorageID = conversation.get('storageID');
const oldStorageVersion = conversation.get('storageVersion');
2020-09-09 02:25:05 +00:00
conversation.set({
2022-03-04 21:14:52 +00:00
hideStory: Boolean(groupV2Record.hideStory),
2020-09-09 02:25:05 +00:00
isArchived: Boolean(groupV2Record.archived),
markedUnread: Boolean(groupV2Record.markedUnread),
2021-08-05 12:35:33 +00:00
dontNotifyForMentionsIfMuted: Boolean(
groupV2Record.dontNotifyForMentionsIfMuted
),
2020-09-09 02:25:05 +00:00
storageID,
2022-02-08 18:00:18 +00:00
storageVersion,
2020-09-09 02:25:05 +00:00
});
2021-04-09 16:19:38 +00:00
conversation.setMuteExpiration(
getTimestampFromLong(groupV2Record.mutedUntilTimestamp),
{
viaStorageServiceSync: true,
}
);
2020-09-09 02:25:05 +00:00
applyMessageRequestState(groupV2Record, conversation);
2022-02-08 18:00:18 +00:00
let details = new Array<string>();
2020-09-09 02:25:05 +00:00
2022-02-08 18:00:18 +00:00
addUnknownFields(groupV2Record, conversation, details);
const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(
2022-03-04 21:14:52 +00:00
toGroupV2Record(conversation),
2020-09-09 02:25:05 +00:00
groupV2Record,
conversation
);
2022-02-08 18:00:18 +00:00
details = details.concat(extraDetails);
const isGroupNewToUs = !isNumber(conversation.get('revision'));
const isFirstSync = !window.storage.get('storageFetchComplete');
2020-09-09 02:25:05 +00:00
const dropInitialJoinMessage = isFirstSync;
if (isGroupV1(conversation.attributes)) {
2020-11-20 17:30:45 +00:00
// If we found a GroupV1 conversation from this incoming GroupV2 record, we need to
// migrate it!
// We don't await this because this could take a very long time, waiting for queues to
// empty, etc.
waitThenRespondToGroupV2Migration({
conversation,
});
} else if (isGroupNewToUs) {
// We don't need to update GroupV2 groups all the time. We fetch group state the first
// time we hear about these groups, from then on we rely on incoming messages or
// the user opening that conversation.
// We don't await this because this could take a very long time, waiting for queues to
// empty, etc.
waitThenMaybeUpdateGroup(
{
conversation,
dropInitialJoinMessage,
},
{ viaFirstStorageSync: isFirstSync }
);
}
2020-09-09 02:25:05 +00:00
2022-02-08 18:00:18 +00:00
return {
hasConflict,
conversation,
2022-03-09 18:22:34 +00:00
updatedConversations: [conversation],
2022-02-08 18:00:18 +00:00
oldStorageID,
oldStorageVersion,
details,
};
2020-09-09 02:25:05 +00:00
}
2020-09-09 00:56:23 +00:00
export async function mergeContactRecord(
storageID: string,
2022-02-08 18:00:18 +00:00
storageVersion: number,
2021-07-13 18:54:53 +00:00
originalContactRecord: Proto.IContactRecord
2022-02-08 18:00:18 +00:00
): Promise<MergeResultType> {
2021-07-09 19:36:10 +00:00
const contactRecord = {
...originalContactRecord,
serviceUuid: originalContactRecord.serviceUuid
? normalizeUuid(
originalContactRecord.serviceUuid,
'ContactRecord.serviceUuid'
)
: undefined,
};
2020-09-09 00:56:23 +00:00
2022-08-17 22:06:17 +00:00
const isPniSupported = RemoteConfig.isEnabled('desktop.pnp');
2022-03-02 22:53:47 +00:00
const e164 = dropNull(contactRecord.serviceE164);
const uuid = dropNull(contactRecord.serviceUuid);
const pni = isPniSupported ? dropNull(contactRecord.pni) : undefined;
2020-09-09 00:56:23 +00:00
// All contacts must have UUID
if (!uuid) {
return { hasConflict: false, shouldDrop: true, details: ['no uuid'] };
}
if (!isValidUuid(uuid)) {
return { hasConflict: false, shouldDrop: true, details: ['invalid uuid'] };
}
if (window.storage.user.getOurUuidKind(new UUID(uuid)) !== UUIDKind.Unknown) {
return { hasConflict: false, shouldDrop: true, details: ['our own uuid'] };
}
const conversation = window.ConversationController.maybeMergeContacts({
aci: uuid,
2020-09-09 00:56:23 +00:00
e164,
2022-08-10 18:39:04 +00:00
pni,
reason: 'mergeContactRecord',
2020-09-09 00:56:23 +00:00
});
if (!conversation) {
throw new Error(`No conversation for ${storageID}`);
2020-09-09 00:56:23 +00:00
}
2022-08-10 18:39:04 +00:00
// We're going to ignore this; it's likely a PNI-only contact we've already merged
if (conversation.get('uuid') !== uuid) {
log.warn(
`mergeContactRecord: ${conversation.idForLogging()} ` +
`with storageId ${conversation.get('storageID')} ` +
`had uuid that didn't match provided uuid ${uuid}`
);
return {
hasConflict: false,
shouldDrop: true,
details: [],
};
}
2022-03-09 18:22:34 +00:00
let needsProfileFetch = false;
if (contactRecord.profileKey && contactRecord.profileKey.length > 0) {
2022-03-09 18:22:34 +00:00
needsProfileFetch = await conversation.setProfileKey(
Bytes.toBase64(contactRecord.profileKey),
{ viaStorageServiceSync: true }
);
2020-09-09 00:56:23 +00:00
}
let details = new Array<string>();
2022-03-02 22:53:47 +00:00
const remoteName = dropNull(contactRecord.givenName);
const remoteFamilyName = dropNull(contactRecord.familyName);
const localName = conversation.get('profileName');
const localFamilyName = conversation.get('profileFamilyName');
if (
remoteName &&
(localName !== remoteName || localFamilyName !== remoteFamilyName)
) {
// Local name doesn't match remote name, fetch profile
if (localName) {
conversation.getProfiles();
details.push('refreshing profile');
2022-03-02 22:53:47 +00:00
} else {
conversation.set({
profileName: remoteName,
profileFamilyName: remoteFamilyName,
});
details.push('updated profile name');
2022-03-02 22:53:47 +00:00
}
}
2022-04-13 00:50:17 +00:00
if (contactRecord.identityKey) {
const verified = await conversation.safeGetVerified();
const storageServiceVerified = contactRecord.identityState || 0;
const verifiedOptions = {
2021-09-24 00:49:05 +00:00
key: contactRecord.identityKey,
viaStorageServiceSync: true,
};
2021-07-13 18:54:53 +00:00
const STATE_ENUM = Proto.ContactRecord.IdentityState;
2020-09-09 00:56:23 +00:00
if (verified !== storageServiceVerified) {
details.push(`updating verified state to=${verified}`);
}
2022-04-13 00:50:17 +00:00
// Update verified status unconditionally to make sure we will take the
// latest identity key from the manifest.
let keyChange: boolean;
2020-09-09 00:56:23 +00:00
switch (storageServiceVerified) {
case STATE_ENUM.VERIFIED:
keyChange = await conversation.setVerified(verifiedOptions);
2020-09-09 00:56:23 +00:00
break;
case STATE_ENUM.UNVERIFIED:
keyChange = await conversation.setUnverified(verifiedOptions);
2020-09-09 00:56:23 +00:00
break;
default:
keyChange = await conversation.setVerifiedDefault(verifiedOptions);
}
if (keyChange) {
details.push('key changed');
2020-09-09 00:56:23 +00:00
}
}
applyMessageRequestState(contactRecord, conversation);
2022-02-08 18:00:18 +00:00
addUnknownFields(contactRecord, conversation, details);
const oldStorageID = conversation.get('storageID');
const oldStorageVersion = conversation.get('storageVersion');
2020-09-09 00:56:23 +00:00
conversation.set({
2022-03-04 21:14:52 +00:00
hideStory: Boolean(contactRecord.hideStory),
2020-09-09 00:56:23 +00:00
isArchived: Boolean(contactRecord.archived),
markedUnread: Boolean(contactRecord.markedUnread),
2020-09-09 00:56:23 +00:00
storageID,
2022-02-08 18:00:18 +00:00
storageVersion,
2020-09-09 00:56:23 +00:00
});
2021-04-09 16:19:38 +00:00
conversation.setMuteExpiration(
getTimestampFromLong(contactRecord.mutedUntilTimestamp),
{
viaStorageServiceSync: true,
}
);
if (
!contactRecord.unregisteredAtTimestamp ||
contactRecord.unregisteredAtTimestamp.equals(0)
) {
conversation.setRegistered({ fromStorageService: true, shouldSave: false });
} else {
conversation.setUnregistered({
timestamp: getTimestampFromLong(contactRecord.unregisteredAtTimestamp),
fromStorageService: true,
shouldSave: false,
});
}
2022-02-08 18:00:18 +00:00
const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(
2020-09-09 00:56:23 +00:00
await toContactRecord(conversation),
contactRecord,
conversation
);
2022-02-08 18:00:18 +00:00
details = details.concat(extraDetails);
2020-09-09 00:56:23 +00:00
2022-02-08 18:00:18 +00:00
return {
hasConflict,
conversation,
2022-03-09 18:22:34 +00:00
updatedConversations: [conversation],
needsProfileFetch,
2022-02-08 18:00:18 +00:00
oldStorageID,
oldStorageVersion,
details,
};
2020-09-09 00:56:23 +00:00
}
export async function mergeAccountRecord(
storageID: string,
2022-02-08 18:00:18 +00:00
storageVersion: number,
2021-07-13 18:54:53 +00:00
accountRecord: Proto.IAccountRecord
2022-02-08 18:00:18 +00:00
): Promise<MergeResultType> {
let details = new Array<string>();
2020-09-09 00:56:23 +00:00
const {
linkPreviews,
notDiscoverableByPhoneNumber,
2020-09-09 00:56:23 +00:00
noteToSelfArchived,
noteToSelfMarkedUnread,
phoneNumberSharingMode,
2021-04-06 22:54:47 +00:00
pinnedConversations,
2020-09-09 00:56:23 +00:00
profileKey,
readReceipts,
sealedSenderIndicators,
typingIndicators,
preferContactAvatars,
primarySendsSms,
2021-06-01 20:45:43 +00:00
universalExpireTimer,
2021-08-05 23:34:49 +00:00
e164: accountE164,
preferredReactionEmoji: rawPreferredReactionEmoji,
subscriberId,
subscriberCurrencyCode,
displayBadgesOnProfile,
2022-05-25 20:44:05 +00:00
keepMutedChatsArchived,
hasSetMyStoriesPrivacy,
2020-09-09 00:56:23 +00:00
} = accountRecord;
2022-03-09 18:22:34 +00:00
const updatedConversations = new Array<ConversationModel>();
window.storage.put('read-receipt-setting', Boolean(readReceipts));
2020-09-09 00:56:23 +00:00
if (typeof sealedSenderIndicators === 'boolean') {
window.storage.put('sealedSenderIndicators', sealedSenderIndicators);
}
if (typeof typingIndicators === 'boolean') {
window.storage.put('typingIndicators', typingIndicators);
}
if (typeof linkPreviews === 'boolean') {
window.storage.put('linkPreviews', linkPreviews);
}
if (typeof preferContactAvatars === 'boolean') {
const previous = window.storage.get('preferContactAvatars');
window.storage.put('preferContactAvatars', preferContactAvatars);
if (Boolean(previous) !== Boolean(preferContactAvatars)) {
window.ConversationController.forceRerender();
}
}
if (typeof primarySendsSms === 'boolean') {
window.storage.put('primarySendsSms', primarySendsSms);
}
2021-08-06 18:20:11 +00:00
if (typeof accountE164 === 'string' && accountE164) {
2021-08-05 23:34:49 +00:00
window.storage.put('accountE164', accountE164);
window.storage.user.setNumber(accountE164);
}
if (preferredReactionEmoji.canBeSynced(rawPreferredReactionEmoji)) {
const localPreferredReactionEmoji =
window.storage.get('preferredReactionEmoji') || [];
if (!isEqual(localPreferredReactionEmoji, rawPreferredReactionEmoji)) {
2022-02-08 18:00:18 +00:00
log.warn(
'storageService: remote and local preferredReactionEmoji do not match',
localPreferredReactionEmoji.length,
rawPreferredReactionEmoji.length
);
}
window.storage.put('preferredReactionEmoji', rawPreferredReactionEmoji);
}
setUniversalExpireTimer(universalExpireTimer || 0);
2021-06-01 20:45:43 +00:00
const PHONE_NUMBER_SHARING_MODE_ENUM =
2021-07-13 18:54:53 +00:00
Proto.AccountRecord.PhoneNumberSharingMode;
let phoneNumberSharingModeToStore: PhoneNumberSharingMode;
switch (phoneNumberSharingMode) {
case undefined:
case null:
case PHONE_NUMBER_SHARING_MODE_ENUM.EVERYBODY:
phoneNumberSharingModeToStore = PhoneNumberSharingMode.Everybody;
break;
case PHONE_NUMBER_SHARING_MODE_ENUM.CONTACTS_ONLY:
phoneNumberSharingModeToStore = PhoneNumberSharingMode.ContactsOnly;
break;
case PHONE_NUMBER_SHARING_MODE_ENUM.NOBODY:
phoneNumberSharingModeToStore = PhoneNumberSharingMode.Nobody;
break;
default:
assertDev(
false,
`storageService.mergeAccountRecord: Got an unexpected phone number sharing mode: ${phoneNumberSharingMode}. Falling back to default`
);
phoneNumberSharingModeToStore = PhoneNumberSharingMode.Everybody;
break;
}
window.storage.put('phoneNumberSharingMode', phoneNumberSharingModeToStore);
const discoverability = notDiscoverableByPhoneNumber
? PhoneNumberDiscoverability.NotDiscoverable
: PhoneNumberDiscoverability.Discoverable;
window.storage.put('phoneNumberDiscoverability', discoverability);
2020-09-09 00:56:23 +00:00
if (profileKey) {
2021-09-24 00:49:05 +00:00
ourProfileKeyService.set(profileKey);
2020-09-09 00:56:23 +00:00
}
2021-04-06 22:54:47 +00:00
if (pinnedConversations) {
2020-10-10 14:25:17 +00:00
const modelPinnedConversations = window
.getConversations()
.filter(conversation => Boolean(conversation.get('isPinned')));
const modelPinnedConversationIds = modelPinnedConversations.map(
conversation => conversation.get('id')
);
2020-11-09 18:30:05 +00:00
const missingStoragePinnedConversationIds = window.storage
.get('pinnedConversationIds', new Array<string>())
2020-11-09 18:30:05 +00:00
.filter(id => !modelPinnedConversationIds.includes(id));
2020-10-10 14:25:17 +00:00
if (missingStoragePinnedConversationIds.length !== 0) {
2022-02-08 18:00:18 +00:00
log.warn(
2020-10-10 14:25:17 +00:00
'mergeAccountRecord: pinnedConversationIds in storage does not match pinned Conversation models'
);
}
const locallyPinnedConversations = modelPinnedConversations.concat(
missingStoragePinnedConversationIds
.map(conversationId =>
window.ConversationController.get(conversationId)
)
.filter(
(conversation): conversation is ConversationModel =>
conversation !== undefined
)
);
2022-02-08 18:00:18 +00:00
details.push(
`local pinned=${locallyPinnedConversations.length}`,
`remote pinned=${pinnedConversations.length}`
2021-04-06 22:54:47 +00:00
);
2020-10-15 00:36:31 +00:00
2021-04-06 22:54:47 +00:00
const remotelyPinnedConversationPromises = pinnedConversations.map(
2021-07-13 18:54:53 +00:00
async ({ contact, legacyGroupId, groupMasterKey }) => {
let conversation: ConversationModel | undefined;
2021-07-13 18:54:53 +00:00
if (contact) {
conversation = window.ConversationController.lookupOrCreate(contact);
2021-07-13 18:54:53 +00:00
} else if (legacyGroupId && legacyGroupId.length) {
const groupId = Bytes.toBinary(legacyGroupId);
conversation = window.ConversationController.get(groupId);
2021-07-13 18:54:53 +00:00
} else if (groupMasterKey && groupMasterKey.length) {
const groupFields = deriveGroupFields(groupMasterKey);
const groupId = Bytes.toBase64(groupFields.id);
conversation = window.ConversationController.get(groupId);
2021-07-13 18:54:53 +00:00
} else {
log.error(
2021-07-13 18:54:53 +00:00
'storageService.mergeAccountRecord: Invalid identifier received'
);
}
if (!conversation) {
log.error(
2021-07-13 18:54:53 +00:00
'storageService.mergeAccountRecord: missing conversation id.'
);
return undefined;
}
return conversation;
}
);
const remotelyPinnedConversations = (
await Promise.all(remotelyPinnedConversationPromises)
).filter(
(conversation): conversation is ConversationModel =>
conversation !== undefined
);
const remotelyPinnedConversationIds = remotelyPinnedConversations.map(
({ id }) => id
);
const conversationsToUnpin = locallyPinnedConversations.filter(
({ id }) => !remotelyPinnedConversationIds.includes(id)
);
2022-02-08 18:00:18 +00:00
details.push(
`unpinning=${conversationsToUnpin.length}`,
`pinning=${remotelyPinnedConversations.length}`
);
conversationsToUnpin.forEach(conversation => {
2020-10-10 14:25:17 +00:00
conversation.set({ isPinned: false });
2022-03-09 18:22:34 +00:00
updatedConversations.push(conversation);
});
2020-10-10 14:25:17 +00:00
remotelyPinnedConversations.forEach(conversation => {
conversation.set({ isPinned: true, isArchived: false });
2022-03-09 18:22:34 +00:00
updatedConversations.push(conversation);
});
2020-10-02 18:30:43 +00:00
window.storage.put('pinnedConversationIds', remotelyPinnedConversationIds);
}
if (subscriberId instanceof Uint8Array) {
window.storage.put('subscriberId', subscriberId);
}
if (typeof subscriberCurrencyCode === 'string') {
window.storage.put('subscriberCurrencyCode', subscriberCurrencyCode);
}
window.storage.put('displayBadgesOnProfile', Boolean(displayBadgesOnProfile));
2022-05-25 20:44:05 +00:00
window.storage.put('keepMutedChatsArchived', Boolean(keepMutedChatsArchived));
window.storage.put('hasSetMyStoriesPrivacy', Boolean(hasSetMyStoriesPrivacy));
2020-09-09 00:56:23 +00:00
const ourID = window.ConversationController.getOurConversationId();
if (!ourID) {
throw new Error('Could not find ourID');
2020-09-09 00:56:23 +00:00
}
const conversation = await window.ConversationController.getOrCreateAndWait(
ourID,
'private'
);
2022-02-08 18:00:18 +00:00
addUnknownFields(accountRecord, conversation, details);
const oldStorageID = conversation.get('storageID');
const oldStorageVersion = conversation.get('storageVersion');
2020-09-09 00:56:23 +00:00
conversation.set({
isArchived: Boolean(noteToSelfArchived),
markedUnread: Boolean(noteToSelfMarkedUnread),
2020-09-09 00:56:23 +00:00
storageID,
2022-02-08 18:00:18 +00:00
storageVersion,
2020-09-09 00:56:23 +00:00
});
2022-03-09 18:22:34 +00:00
let needsProfileFetch = false;
if (profileKey && profileKey.length > 0) {
2022-03-09 18:22:34 +00:00
needsProfileFetch = await conversation.setProfileKey(
Bytes.toBase64(profileKey),
2022-03-09 18:22:34 +00:00
{ viaStorageServiceSync: true }
);
2020-09-09 00:56:23 +00:00
const avatarUrl = dropNull(accountRecord.avatarUrl);
await conversation.setProfileAvatar(avatarUrl, profileKey);
window.storage.put('avatarUrl', avatarUrl);
2020-09-09 00:56:23 +00:00
}
2022-02-08 18:00:18 +00:00
const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(
2022-03-04 21:14:52 +00:00
toAccountRecord(conversation),
2020-09-09 00:56:23 +00:00
accountRecord,
conversation
);
2022-03-09 18:22:34 +00:00
updatedConversations.push(conversation);
2020-09-09 00:56:23 +00:00
2022-02-08 18:00:18 +00:00
details = details.concat(extraDetails);
return {
hasConflict,
conversation,
2022-03-09 18:22:34 +00:00
updatedConversations,
needsProfileFetch,
2022-02-08 18:00:18 +00:00
oldStorageID,
oldStorageVersion,
details,
};
2020-09-09 00:56:23 +00:00
}
2022-07-01 00:52:03 +00:00
export async function mergeStoryDistributionListRecord(
storageID: string,
storageVersion: number,
storyDistributionListRecord: Proto.IStoryDistributionListRecord
): Promise<MergeResultType> {
if (!storyDistributionListRecord.identifier) {
throw new Error(`No storyDistributionList identifier for ${storageID}`);
}
const details: Array<string> = [];
2022-08-11 19:18:48 +00:00
const isMyStories = Bytes.areEqual(
MY_STORIES_BYTES,
storyDistributionListRecord.identifier
2022-08-11 19:18:48 +00:00
);
const listId = isMyStories
? MY_STORIES_ID
: bytesToUuid(storyDistributionListRecord.identifier);
2022-07-01 00:52:03 +00:00
if (!listId) {
throw new Error('Could not parse distribution list id');
}
const localStoryDistributionList =
await dataInterface.getStoryDistributionWithMembers(listId);
const remoteListMembers: Array<UUIDStringType> = (
storyDistributionListRecord.recipientUuids || []
).map(UUID.cast);
2022-07-01 00:52:03 +00:00
if (storyDistributionListRecord.__unknownFields) {
details.push('adding unknown fields');
}
const deletedAtTimestamp = getTimestampFromLong(
storyDistributionListRecord.deletedAtTimestamp
);
2022-07-01 00:52:03 +00:00
const storyDistribution: StoryDistributionWithMembersType = {
id: listId,
name: String(storyDistributionListRecord.name),
deletedAtTimestamp: isMyStories ? undefined : deletedAtTimestamp,
2022-07-01 00:52:03 +00:00
allowsReplies: Boolean(storyDistributionListRecord.allowsReplies),
isBlockList: Boolean(storyDistributionListRecord.isBlockList),
members: remoteListMembers,
senderKeyInfo: localStoryDistributionList?.senderKeyInfo,
storageID,
storageVersion,
storageUnknownFields: storyDistributionListRecord.__unknownFields
? Bytes.concatenate(storyDistributionListRecord.__unknownFields)
: null,
storageNeedsSync: Boolean(localStoryDistributionList?.storageNeedsSync),
};
if (!localStoryDistributionList) {
await dataInterface.createNewStoryDistribution(storyDistribution);
const shouldSave = false;
window.reduxActions.storyDistributionLists.createDistributionList(
storyDistribution.name,
remoteListMembers,
storyDistribution,
shouldSave
);
2022-07-01 00:52:03 +00:00
return {
details,
hasConflict: false,
};
}
const oldStorageID = localStoryDistributionList.storageID;
const oldStorageVersion = localStoryDistributionList.storageVersion;
const needsToClearUnknownFields =
!storyDistributionListRecord.__unknownFields &&
localStoryDistributionList.storageUnknownFields;
if (needsToClearUnknownFields) {
details.push('clearing unknown fields');
}
const isBadRemoteData = !deletedAtTimestamp && !storyDistribution.name;
if (isBadRemoteData) {
Object.assign(storyDistribution, {
name: localStoryDistributionList.name,
members: localStoryDistributionList.members,
});
}
2022-07-01 00:52:03 +00:00
const { hasConflict, details: conflictDetails } = doRecordsConflict(
toStoryDistributionListRecord(storyDistribution),
storyDistributionListRecord
);
const localMembersListSet = new Set(localStoryDistributionList.members);
const toAdd: Array<UUIDStringType> = remoteListMembers.filter(
uuid => !localMembersListSet.has(uuid)
);
const remoteMemberListSet = new Set(remoteListMembers);
const toRemove: Array<UUIDStringType> =
localStoryDistributionList.members.filter(
uuid => !remoteMemberListSet.has(uuid)
);
const needsUpdate = Boolean(
needsToClearUnknownFields || hasConflict || toAdd.length || toRemove.length
);
2022-07-01 00:52:03 +00:00
if (!needsUpdate) {
return {
details: [...details, ...conflictDetails],
hasConflict,
oldStorageID,
oldStorageVersion,
};
}
if (needsUpdate) {
await dataInterface.modifyStoryDistributionWithMembers(storyDistribution, {
toAdd,
toRemove,
});
window.reduxActions.storyDistributionLists.modifyDistributionList({
allowsReplies: Boolean(storyDistribution.allowsReplies),
deletedAtTimestamp: storyDistribution.deletedAtTimestamp,
2022-07-01 00:52:03 +00:00
id: storyDistribution.id,
isBlockList: Boolean(storyDistribution.isBlockList),
membersToAdd: toAdd,
membersToRemove: toRemove,
2022-07-01 00:52:03 +00:00
name: storyDistribution.name,
});
}
return {
details: [...details, ...conflictDetails],
hasConflict,
oldStorageID,
oldStorageVersion,
};
}
2022-08-03 17:10:49 +00:00
export async function mergeStickerPackRecord(
storageID: string,
storageVersion: number,
stickerPackRecord: Proto.IStickerPackRecord
): Promise<MergeResultType> {
if (!stickerPackRecord.packId || Bytes.isEmpty(stickerPackRecord.packId)) {
throw new Error(`No stickerPackRecord identifier for ${storageID}`);
}
const details: Array<string> = [];
const id = Bytes.toHex(stickerPackRecord.packId);
const localStickerPack = await dataInterface.getStickerPackInfo(id);
if (stickerPackRecord.__unknownFields) {
details.push('adding unknown fields');
}
const storageUnknownFields = stickerPackRecord.__unknownFields
? Bytes.concatenate(stickerPackRecord.__unknownFields)
: null;
let stickerPack: StickerPackInfoType;
if (stickerPackRecord.deletedAtTimestamp?.toNumber()) {
stickerPack = {
id,
uninstalledAt: stickerPackRecord.deletedAtTimestamp.toNumber(),
storageID,
storageVersion,
storageUnknownFields,
storageNeedsSync: false,
};
} else {
if (
!stickerPackRecord.packKey ||
Bytes.isEmpty(stickerPackRecord.packKey)
) {
throw new Error(`No stickerPackRecord key for ${storageID}`);
}
stickerPack = {
id,
key: Bytes.toBase64(stickerPackRecord.packKey),
position:
'position' in stickerPackRecord
? stickerPackRecord.position
: localStickerPack?.position ?? undefined,
storageID,
storageVersion,
storageUnknownFields,
storageNeedsSync: false,
};
}
const oldStorageID = localStickerPack?.storageID;
const oldStorageVersion = localStickerPack?.storageVersion;
const needsToClearUnknownFields =
!stickerPack.storageUnknownFields && localStickerPack?.storageUnknownFields;
if (needsToClearUnknownFields) {
details.push('clearing unknown fields');
}
const { hasConflict, details: conflictDetails } = doRecordsConflict(
toStickerPackRecord(stickerPack),
stickerPackRecord
);
const wasUninstalled = Boolean(localStickerPack?.uninstalledAt);
const isUninstalled = Boolean(stickerPack.uninstalledAt);
details.push(
`wasUninstalled=${wasUninstalled}`,
`isUninstalled=${isUninstalled}`,
`oldPosition=${localStickerPack?.position ?? '?'}`,
`newPosition=${stickerPack.position ?? '?'}`
);
if (localStickerPack && !wasUninstalled && isUninstalled) {
assertDev(localStickerPack.key, 'Installed sticker pack has no key');
2022-08-03 17:10:49 +00:00
window.reduxActions.stickers.uninstallStickerPack(
localStickerPack.id,
localStickerPack.key,
{ fromStorageService: true }
);
} else if ((!localStickerPack || wasUninstalled) && !isUninstalled) {
assertDev(stickerPack.key, 'Sticker pack does not have key');
2022-08-03 17:10:49 +00:00
const status = Stickers.getStickerPackStatus(stickerPack.id);
if (status === 'downloaded') {
window.reduxActions.stickers.installStickerPack(
stickerPack.id,
stickerPack.key,
{
fromStorageService: true,
}
);
} else {
Stickers.downloadStickerPack(stickerPack.id, stickerPack.key, {
finalStatus: 'installed',
fromStorageService: true,
});
}
}
await dataInterface.updateStickerPackInfo(stickerPack);
return {
details: [...details, ...conflictDetails],
hasConflict,
oldStorageID,
oldStorageVersion,
};
}