signal-desktop/ts/services/storageRecordOps.ts

1909 lines
59 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2020 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
2023-08-09 00:53:06 +00:00
import { uuidToBytes, bytesToUuid } from '../util/uuidToBytes';
import { 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, strictAssert } from '../util/assert';
2022-03-02 22:53:47 +00:00
import { dropNull } from '../util/dropNull';
import { missingCaseError } from '../util/missingCaseError';
2023-02-08 17:19:13 +00:00
import { isNotNil } from '../util/isNotNil';
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';
import { canHaveUsername } from '../util/getTitle';
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';
2022-11-16 20:18:02 +00:00
import { DurationInSeconds } from '../util/durations';
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';
import { normalizeStoryDistributionId } from '../types/StoryDistributionId';
import type { StoryDistributionIdString } from '../types/StoryDistributionId';
import type { ServiceIdString } from '../types/ServiceId';
import {
normalizeServiceId,
normalizePni,
ServiceIdKind,
2023-09-27 23:14:55 +00:00
isUntaggedPniString,
toUntaggedPni,
toTaggedPni,
} from '../types/ServiceId';
2023-09-14 17:04:48 +00:00
import { normalizeAci } from '../util/normalizeAci';
import { isAciString } from '../util/isAciString';
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_STORY_ID, StorySendMode } from '../types/Stories';
2022-11-09 02:38:19 +00:00
import { findAndDeleteOnboardingStoryIfExists } from '../util/findAndDeleteOnboardingStoryIfExists';
import { downloadOnboardingStory } from '../util/downloadOnboardingStory';
import { drop } from '../util/drop';
import { redactExtendedStorageID } from '../util/privacy';
const MY_STORY_BYTES = uuidToBytes(MY_STORY_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 fromRecordVerified(
verified: Proto.ContactRecord.IdentityState
): number {
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
const STATE_ENUM = Proto.ContactRecord.IdentityState;
switch (verified) {
case STATE_ENUM.VERIFIED:
return VERIFIED_ENUM.VERIFIED;
case STATE_ENUM.UNVERIFIED:
return VERIFIED_ENUM.UNVERIFIED;
default:
return VERIFIED_ENUM.DEFAULT;
}
}
2020-09-09 00:56:23 +00:00
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)
2021-07-13 18:54:53 +00:00
),
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
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();
const aci = conversation.getAci();
if (aci) {
contactRecord.aci = aci;
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
}
const username = conversation.get('username');
const ourID = window.ConversationController.getOurConversationId();
if (username && canHaveUsername(conversation.attributes, ourID)) {
contactRecord.username = username;
}
const pni = conversation.getPni();
if (pni) {
2023-09-27 23:14:55 +00:00
contactRecord.pni = toUntaggedPni(pni);
2022-08-10 18:39:04 +00:00
}
2024-01-29 22:37:26 +00:00
contactRecord.pniSignatureVerified =
conversation.get('pniSignatureVerified') ?? false;
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
}
const serviceId = aci ?? pni;
const identityKey = serviceId
? await window.textsecure.storage.protocol.loadIdentityKey(serviceId)
: 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
}
2024-03-26 19:48:33 +00:00
const nicknameGivenName = conversation.get('nicknameGivenName');
const nicknameFamilyName = conversation.get('nicknameFamilyName');
if (nicknameGivenName || nicknameFamilyName) {
2024-03-26 19:48:33 +00:00
contactRecord.nickname = {
given: nicknameGivenName,
family: nicknameFamilyName,
};
}
const note = conversation.get('note');
if (note) {
contactRecord.note = note;
}
const systemGivenName = conversation.get('systemGivenName');
if (systemGivenName) {
contactRecord.systemGivenName = systemGivenName;
}
const systemFamilyName = conversation.get('systemFamilyName');
if (systemFamilyName) {
contactRecord.systemFamilyName = systemFamilyName;
}
2023-02-13 22:40:11 +00:00
const systemNickname = conversation.get('systemNickname');
if (systemNickname) {
contactRecord.systemNickname = systemNickname;
}
2020-09-09 00:56:23 +00:00
contactRecord.blocked = conversation.isBlocked();
2023-04-05 20:48:00 +00:00
contactRecord.hidden = conversation.get('removalStage') !== undefined;
2020-09-09 00:56:23 +00:00
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;
}
2023-02-08 17:14:59 +00:00
const username = conversation.get('username');
if (username !== undefined) {
accountRecord.username = username;
}
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);
}
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:
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 = {
2023-08-16 20:54:39 +00:00
serviceId: pinnedConversation.getServiceId(),
2020-10-02 18:30:43 +00:00
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 (Bytes.isNotEmpty(subscriberId)) {
accountRecord.subscriberId = subscriberId;
}
const subscriberCurrencyCode = window.storage.get(
'backupsSubscriberCurrencyCode'
);
if (typeof subscriberCurrencyCode === 'string') {
accountRecord.subscriberCurrencyCode = subscriberCurrencyCode;
}
const donorSubscriptionManuallyCancelled = window.storage.get(
'donorSubscriptionManuallyCancelled'
);
if (typeof donorSubscriptionManuallyCancelled === 'boolean') {
accountRecord.donorSubscriptionManuallyCancelled =
donorSubscriptionManuallyCancelled;
}
const backupsSubscriberId = window.storage.get('backupsSubscriberId');
if (Bytes.isNotEmpty(backupsSubscriberId)) {
accountRecord.backupsSubscriberId = backupsSubscriberId;
}
const backupsSubscriberCurrencyCode = window.storage.get(
'backupsSubscriberCurrencyCode'
);
if (typeof backupsSubscriberCurrencyCode === 'string') {
accountRecord.backupsSubscriberCurrencyCode = backupsSubscriberCurrencyCode;
}
const backupsSubscriptionManuallyCancelled = window.storage.get(
'backupsSubscriptionManuallyCancelled'
);
if (typeof backupsSubscriptionManuallyCancelled === 'boolean') {
accountRecord.backupsSubscriptionManuallyCancelled =
backupsSubscriptionManuallyCancelled;
}
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;
}
2022-11-09 02:38:19 +00:00
const hasViewedOnboardingStory = window.storage.get(
'hasViewedOnboardingStory'
);
if (hasViewedOnboardingStory !== undefined) {
accountRecord.hasViewedOnboardingStory = hasViewedOnboardingStory;
}
2023-02-13 18:51:41 +00:00
const hasCompletedUsernameOnboarding = window.storage.get(
'hasCompletedUsernameOnboarding'
);
if (hasCompletedUsernameOnboarding !== undefined) {
accountRecord.hasCompletedUsernameOnboarding =
hasCompletedUsernameOnboarding;
}
const hasSeenGroupStoryEducationSheet = window.storage.get(
'hasSeenGroupStoryEducationSheet'
);
if (hasSeenGroupStoryEducationSheet !== undefined) {
accountRecord.hasSeenGroupStoryEducationSheet =
hasSeenGroupStoryEducationSheet;
}
const hasStoriesDisabled = window.storage.get('hasStoriesDisabled');
accountRecord.storiesDisabled = hasStoriesDisabled === true;
2022-10-25 22:18:42 +00:00
const storyViewReceiptsEnabled = window.storage.get(
'storyViewReceiptsEnabled'
);
if (storyViewReceiptsEnabled !== undefined) {
accountRecord.storyViewReceiptsEnabled = storyViewReceiptsEnabled
? Proto.OptionalBool.ENABLED
: Proto.OptionalBool.DISABLED;
} else {
accountRecord.storyViewReceiptsEnabled = Proto.OptionalBool.UNSET;
}
2023-07-20 03:14:08 +00:00
// Username link
{
const color = window.storage.get('usernameLinkColor');
const linkData = window.storage.get('usernameLink');
if (linkData?.entropy.length && linkData?.serverId.length) {
accountRecord.usernameLink = {
color,
entropy: linkData.entropy,
serverId: linkData.serverId,
};
}
}
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'));
const storySendMode = conversation.get('storySendMode');
if (storySendMode !== undefined) {
if (storySendMode === StorySendMode.IfActive) {
groupV2Record.storySendMode = Proto.GroupV2Record.StorySendMode.DEFAULT;
} else if (storySendMode === StorySendMode.Never) {
groupV2Record.storySendMode = Proto.GroupV2Record.StorySendMode.DISABLED;
} else if (storySendMode === StorySendMode.Always) {
groupV2Record.storySendMode = Proto.GroupV2Record.StorySendMode.ENABLED;
} else {
throw missingCaseError(storySendMode);
}
}
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.recipientServiceIds =
storyDistributionList.members;
2022-07-01 00:52:03 +00:00
if (storyDistributionList.storageUnknownFields) {
storyDistributionListRecord.$unknownFields = [
2022-07-01 00:52:03 +00:00
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];
2022-08-03 17:10:49 +00:00
}
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) {
void conversation.applyMessageRequestResponse(messageRequestEnum.BLOCK, {
2020-10-06 17:06:34 +00:00
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
void conversation.applyMessageRequestResponse(messageRequestEnum.ACCEPT, {
2020-10-06 17:06:34 +00:00
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 (
(!remoteValue || (Long.isLong(remoteValue) && remoteValue.isZero())) &&
(!localValue || (Long.isLong(localValue) && localValue.isZero()))
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> {
const redactedStorageID = redactExtendedStorageID({
storageID,
storageVersion,
});
2020-09-09 00:56:23 +00:00
if (!groupV1Record.id) {
throw new Error(`No ID for ${redactedStorageID}`);
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> {
const redactedStorageID = redactExtendedStorageID({
storageID,
storageVersion,
});
2020-11-20 17:30:45 +00:00
if (!groupV2Record.masterKey) {
throw new Error(`No master key for ${redactedStorageID}`);
2020-11-20 17:30:45 +00:00
}
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');
const recordStorySendMode =
groupV2Record.storySendMode ?? Proto.GroupV2Record.StorySendMode.DEFAULT;
let storySendMode: StorySendMode;
if (recordStorySendMode === Proto.GroupV2Record.StorySendMode.DEFAULT) {
storySendMode = StorySendMode.IfActive;
} else if (
recordStorySendMode === Proto.GroupV2Record.StorySendMode.DISABLED
) {
storySendMode = StorySendMode.Never;
} else if (
recordStorySendMode === Proto.GroupV2Record.StorySendMode.ENABLED
) {
storySendMode = StorySendMode.Always;
} else {
throw missingCaseError(recordStorySendMode);
}
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,
storySendMode,
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.
void waitThenRespondToGroupV2Migration({
2020-11-20 17:30:45 +00:00
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.
void 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,
aci: originalContactRecord.aci
? normalizeAci(originalContactRecord.aci, 'ContactRecord.aci')
: undefined,
2023-09-27 23:14:55 +00:00
pni:
originalContactRecord.pni &&
isUntaggedPniString(originalContactRecord.pni)
? normalizePni(
toTaggedPni(originalContactRecord.pni),
'ContactRecord.pni'
)
: undefined,
2021-07-09 19:36:10 +00:00
};
2020-09-09 00:56:23 +00:00
2022-03-02 22:53:47 +00:00
const e164 = dropNull(contactRecord.serviceE164);
const { aci } = contactRecord;
const pni = dropNull(contactRecord.pni);
2024-01-29 22:37:26 +00:00
const pniSignatureVerified = contactRecord.pniSignatureVerified || false;
const serviceId = aci || pni;
2020-09-09 00:56:23 +00:00
// All contacts must have UUID
if (!serviceId) {
return { hasConflict: false, shouldDrop: true, details: ['no uuid'] };
}
// Contacts should not have PNI as ACI
if (aci && !isAciString(aci)) {
return { hasConflict: false, shouldDrop: true, details: ['invalid aci'] };
}
if (
window.storage.user.getOurServiceIdKind(serviceId) !== ServiceIdKind.Unknown
) {
return { hasConflict: false, shouldDrop: true, details: ['our own uuid'] };
}
const { conversation } = window.ConversationController.maybeMergeContacts({
aci,
2020-09-09 00:56:23 +00:00
e164,
2022-08-10 18:39:04 +00:00
pni,
2024-01-29 22:37:26 +00:00
fromPniSignature: pniSignatureVerified,
reason: 'mergeContactRecord',
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
2023-08-16 20:54:39 +00:00
if (conversation.getServiceId() !== serviceId) {
const previousStorageID = conversation.get('storageID');
const redactedpreviousStorageID = previousStorageID
? redactExtendedStorageID({
storageID: previousStorageID,
storageVersion: conversation.get('storageVersion'),
})
: '<none>';
2022-08-10 18:39:04 +00:00
log.warn(
`mergeContactRecord: ${conversation.idForLogging()} ` +
`with storageId ${redactedpreviousStorageID} ` +
`had serviceId that didn't match provided serviceId ${serviceId}`
2022-08-10 18:39:04 +00:00
);
return {
hasConflict: false,
shouldDrop: true,
details: [],
};
}
await conversation.updateUsername(dropNull(contactRecord.username), {
shouldSave: false,
});
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) {
drop(
conversation.getProfiles().catch(() => {
/* nothing to do here; logging already happened */
})
);
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
}
}
conversation.set({
systemGivenName: dropNull(contactRecord.systemGivenName),
systemFamilyName: dropNull(contactRecord.systemFamilyName),
2023-02-13 22:40:11 +00:00
systemNickname: dropNull(contactRecord.systemNickname),
2024-03-26 19:48:33 +00:00
nicknameGivenName: dropNull(contactRecord.nickname?.given),
nicknameFamilyName: dropNull(contactRecord.nickname?.family),
note: dropNull(contactRecord.note),
});
2022-03-02 22:53:47 +00:00
// https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt#L921-L936
2022-04-13 00:50:17 +00:00
if (contactRecord.identityKey) {
const verified = await conversation.safeGetVerified();
let { identityState } = contactRecord;
if (identityState == null) {
details.push('identity state was null, reverting to default state');
identityState = Proto.ContactRecord.IdentityState.DEFAULT;
}
const newVerified = fromRecordVerified(identityState);
2020-09-09 00:56:23 +00:00
const needsNotification =
await window.textsecure.storage.protocol.updateIdentityAfterSync(
serviceId,
newVerified,
contactRecord.identityKey
);
if (verified !== newVerified) {
details.push(
`updating verified state from=${verified} to=${newVerified}`
);
conversation.set({ verified: newVerified });
}
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
if (needsNotification) {
details.push('adding a verified notification');
await conversation.addVerifiedChange(
conversation.id,
newVerified === VERIFIED_ENUM.VERIFIED,
{ local: false }
);
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
});
2023-04-05 20:48:00 +00:00
if (contactRecord.hidden) {
await conversation.removeContact({
viaStorageServiceSync: true,
shouldSave: false,
});
} else {
await conversation.restoreContact({
viaStorageServiceSync: true,
shouldSave: false,
});
}
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,
preferredReactionEmoji: rawPreferredReactionEmoji,
subscriberId,
subscriberCurrencyCode,
donorSubscriptionManuallyCancelled,
backupsSubscriberId,
backupsSubscriberCurrencyCode,
backupsSubscriptionManuallyCancelled,
displayBadgesOnProfile,
2022-05-25 20:44:05 +00:00
keepMutedChatsArchived,
2023-02-13 18:51:41 +00:00
hasCompletedUsernameOnboarding,
hasSeenGroupStoryEducationSheet,
hasSetMyStoriesPrivacy,
2022-11-09 02:38:19 +00:00
hasViewedOnboardingStory,
storiesDisabled,
2022-10-25 22:18:42 +00:00
storyViewReceiptsEnabled,
2023-02-08 17:14:59 +00:00
username,
2023-07-20 03:14:08 +00:00
usernameLink,
2020-09-09 00:56:23 +00:00
} = accountRecord;
2022-03-09 18:22:34 +00:00
const updatedConversations = new Array<ConversationModel>();
await window.storage.put('read-receipt-setting', Boolean(readReceipts));
2020-09-09 00:56:23 +00:00
if (typeof sealedSenderIndicators === 'boolean') {
await window.storage.put('sealedSenderIndicators', sealedSenderIndicators);
2020-09-09 00:56:23 +00:00
}
if (typeof typingIndicators === 'boolean') {
await window.storage.put('typingIndicators', typingIndicators);
2020-09-09 00:56:23 +00:00
}
if (typeof linkPreviews === 'boolean') {
await window.storage.put('linkPreviews', linkPreviews);
2020-09-09 00:56:23 +00:00
}
if (typeof preferContactAvatars === 'boolean') {
const previous = window.storage.get('preferContactAvatars');
await window.storage.put('preferContactAvatars', preferContactAvatars);
if (Boolean(previous) !== Boolean(preferContactAvatars)) {
await window.ConversationController.forceRerender();
}
}
if (typeof primarySendsSms === 'boolean') {
await window.storage.put('primarySendsSms', primarySendsSms);
}
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
);
}
await window.storage.put(
'preferredReactionEmoji',
rawPreferredReactionEmoji
);
}
void setUniversalExpireTimer(
2022-11-16 20:18:02 +00:00
DurationInSeconds.fromSeconds(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;
2024-02-22 17:29:58 +00:00
case PHONE_NUMBER_SHARING_MODE_ENUM.UNKNOWN:
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;
}
await window.storage.put(
'phoneNumberSharingMode',
phoneNumberSharingModeToStore
);
const discoverability = notDiscoverableByPhoneNumber
? PhoneNumberDiscoverability.NotDiscoverable
: PhoneNumberDiscoverability.Discoverable;
await window.storage.put('phoneNumberDiscoverability', discoverability);
2020-09-09 00:56:23 +00:00
if (profileKey) {
void 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
2023-02-08 17:19:13 +00:00
const remotelyPinnedConversations = pinnedConversations
.map(({ contact, legacyGroupId, groupMasterKey }) => {
let conversation: ConversationModel | undefined;
2021-07-13 18:54:53 +00:00
if (contact) {
2023-08-16 20:54:39 +00:00
if (!contact.serviceId && !contact.e164) {
log.error(
2023-08-16 20:54:39 +00:00
'storageService.mergeAccountRecord: No serviceId or e164 on contact'
);
return undefined;
}
conversation = window.ConversationController.lookupOrCreate({
2023-08-16 20:54:39 +00:00
serviceId: contact.serviceId
? normalizeServiceId(
contact.serviceId,
'AccountRecord.pin.serviceId'
)
: undefined,
e164: contact.e164,
reason: 'storageService.mergeAccountRecord',
});
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;
2023-02-08 17:19:13 +00:00
})
.filter(isNotNil);
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
await window.storage.put(
'pinnedConversationIds',
remotelyPinnedConversationIds
);
}
if (Bytes.isNotEmpty(subscriberId)) {
await window.storage.put('subscriberId', subscriberId);
}
if (typeof subscriberCurrencyCode === 'string') {
await window.storage.put('subscriberCurrencyCode', subscriberCurrencyCode);
}
if (donorSubscriptionManuallyCancelled != null) {
await window.storage.put(
'donorSubscriptionManuallyCancelled',
donorSubscriptionManuallyCancelled
);
}
if (Bytes.isNotEmpty(backupsSubscriberId)) {
await window.storage.put('backupsSubscriberId', backupsSubscriberId);
}
if (typeof backupsSubscriberCurrencyCode === 'string') {
await window.storage.put(
'backupsSubscriberCurrencyCode',
backupsSubscriberCurrencyCode
);
}
if (backupsSubscriptionManuallyCancelled != null) {
await window.storage.put(
'backupsSubscriptionManuallyCancelled',
backupsSubscriptionManuallyCancelled
);
}
await window.storage.put(
'displayBadgesOnProfile',
Boolean(displayBadgesOnProfile)
);
await window.storage.put(
'keepMutedChatsArchived',
Boolean(keepMutedChatsArchived)
);
await window.storage.put(
'hasSetMyStoriesPrivacy',
Boolean(hasSetMyStoriesPrivacy)
);
2022-11-09 02:38:19 +00:00
{
const hasViewedOnboardingStoryBool = Boolean(hasViewedOnboardingStory);
await window.storage.put(
2022-11-09 02:38:19 +00:00
'hasViewedOnboardingStory',
hasViewedOnboardingStoryBool
);
if (hasViewedOnboardingStoryBool) {
drop(findAndDeleteOnboardingStoryIfExists());
} else {
drop(downloadOnboardingStory());
2022-11-09 02:38:19 +00:00
}
}
2023-02-13 18:51:41 +00:00
{
const hasCompletedUsernameOnboardingBool = Boolean(
hasCompletedUsernameOnboarding
);
await window.storage.put(
'hasCompletedUsernameOnboarding',
hasCompletedUsernameOnboardingBool
);
}
{
const hasCompletedUsernameOnboardingBool = Boolean(
hasSeenGroupStoryEducationSheet
);
await window.storage.put(
'hasSeenGroupStoryEducationSheet',
hasCompletedUsernameOnboardingBool
);
}
2022-10-05 00:48:25 +00:00
{
const hasStoriesDisabled = Boolean(storiesDisabled);
await window.storage.put('hasStoriesDisabled', hasStoriesDisabled);
2022-10-05 00:48:25 +00:00
window.textsecure.server?.onHasStoriesDisabledChange(hasStoriesDisabled);
}
2022-10-25 22:18:42 +00:00
switch (storyViewReceiptsEnabled) {
case Proto.OptionalBool.ENABLED:
await window.storage.put('storyViewReceiptsEnabled', true);
2022-10-25 22:18:42 +00:00
break;
case Proto.OptionalBool.DISABLED:
await window.storage.put('storyViewReceiptsEnabled', false);
2022-10-25 22:18:42 +00:00
break;
case Proto.OptionalBool.UNSET:
default:
// Do nothing
break;
}
2023-07-20 03:14:08 +00:00
if (usernameLink?.entropy?.length && usernameLink?.serverId?.length) {
const oldLink = window.storage.get('usernameLink');
if (
window.storage.get('usernameLinkCorrupted') &&
(!oldLink ||
!Bytes.areEqual(usernameLink.entropy, oldLink.entropy) ||
!Bytes.areEqual(usernameLink.serverId, oldLink.serverId))
) {
details.push('clearing username link corruption');
await window.storage.remove('usernameLinkCorrupted');
}
2023-07-20 03:14:08 +00:00
await Promise.all([
usernameLink.color &&
window.storage.put('usernameLinkColor', usernameLink.color),
window.storage.put('usernameLink', {
entropy: usernameLink.entropy,
serverId: usernameLink.serverId,
}),
]);
} else {
await Promise.all([
window.storage.remove('usernameLinkColor'),
window.storage.remove('usernameLink'),
]);
}
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
if (
window.storage.get('usernameCorrupted') &&
username !== conversation.get('username')
) {
details.push('clearing username corruption');
await window.storage.remove('usernameCorrupted');
}
2020-09-09 00:56:23 +00:00
conversation.set({
isArchived: Boolean(noteToSelfArchived),
markedUnread: Boolean(noteToSelfMarkedUnread),
2023-02-08 17:14:59 +00:00
username: dropNull(username),
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);
await 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> {
const redactedStorageID = redactExtendedStorageID({
storageID,
storageVersion,
});
2022-07-01 00:52:03 +00:00
if (!storyDistributionListRecord.identifier) {
throw new Error(
`No storyDistributionList identifier for ${redactedStorageID}`
);
2022-07-01 00:52:03 +00:00
}
const details: Array<string> = [];
const isMyStory = Bytes.areEqual(
MY_STORY_BYTES,
storyDistributionListRecord.identifier
2022-08-11 19:18:48 +00:00
);
let listId: StoryDistributionIdString;
if (isMyStory) {
listId = MY_STORY_ID;
} else {
const uuid = bytesToUuid(storyDistributionListRecord.identifier);
strictAssert(uuid, 'mergeStoryDistributionListRecord: no distribution id');
listId = normalizeStoryDistributionId(
uuid,
'mergeStoryDistributionListRecord'
);
2022-07-01 00:52:03 +00:00
}
const localStoryDistributionList =
await dataInterface.getStoryDistributionWithMembers(listId);
const remoteListMembers: Array<ServiceIdString> = (
storyDistributionListRecord.recipientServiceIds || []
).map(id => normalizeServiceId(id, 'mergeStoryDistributionListRecord'));
2022-07-01 00:52:03 +00:00
if (storyDistributionListRecord.$unknownFields) {
2022-07-01 00:52:03 +00:00
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: isMyStory ? 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)
2022-07-01 00:52:03 +00:00
: 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 &&
2022-07-01 00:52:03 +00:00
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<ServiceIdString> = remoteListMembers.filter(
2023-08-16 20:54:39 +00:00
serviceId => !localMembersListSet.has(serviceId)
2022-07-01 00:52:03 +00:00
);
const remoteMemberListSet = new Set(remoteListMembers);
const toRemove: Array<ServiceIdString> =
2022-07-01 00:52:03 +00:00
localStoryDistributionList.members.filter(
2023-08-16 20:54:39 +00:00
serviceId => !remoteMemberListSet.has(serviceId)
2022-07-01 00:52:03 +00:00
);
details.push('updated');
await dataInterface.modifyStoryDistributionWithMembers(storyDistribution, {
toAdd,
toRemove,
});
window.reduxActions.storyDistributionLists.modifyDistributionList({
allowsReplies: Boolean(storyDistribution.allowsReplies),
deletedAtTimestamp: storyDistribution.deletedAtTimestamp,
id: storyDistribution.id,
isBlockList: Boolean(storyDistribution.isBlockList),
membersToAdd: toAdd,
membersToRemove: toRemove,
name: storyDistribution.name,
});
2022-07-01 00:52:03 +00:00
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> {
const redactedStorageID = redactExtendedStorageID({
storageID,
storageVersion,
});
2022-08-03 17:10:49 +00:00
if (!stickerPackRecord.packId || Bytes.isEmpty(stickerPackRecord.packId)) {
throw new Error(`No stickerPackRecord identifier for ${redactedStorageID}`);
2022-08-03 17:10:49 +00:00
}
const details: Array<string> = [];
const id = Bytes.toHex(stickerPackRecord.packId);
const localStickerPack = await dataInterface.getStickerPackInfo(id);
if (stickerPackRecord.$unknownFields) {
2022-08-03 17:10:49 +00:00
details.push('adding unknown fields');
}
const storageUnknownFields = stickerPackRecord.$unknownFields
? Bytes.concatenate(stickerPackRecord.$unknownFields)
2022-08-03 17:10:49 +00:00
: 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 ${redactedStorageID}`);
2022-08-03 17:10:49 +00:00
}
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 {
void Stickers.downloadStickerPack(stickerPack.id, stickerPack.key, {
2022-08-03 17:10:49 +00:00
finalStatus: 'installed',
fromStorageService: true,
});
}
}
await dataInterface.updateStickerPackInfo(stickerPack);
return {
details: [...details, ...conflictDetails],
hasConflict,
oldStorageID,
oldStorageVersion,
};
}