Storage Service: Write
This commit is contained in:
parent
8a2c17f65f
commit
1ce0959fa1
15 changed files with 1374 additions and 540 deletions
403
ts/services/storageRecordOps.ts
Normal file
403
ts/services/storageRecordOps.ts
Normal file
|
@ -0,0 +1,403 @@
|
|||
/* tslint:disable no-backbone-get-set-outside-model */
|
||||
import _ from 'lodash';
|
||||
|
||||
import {
|
||||
arrayBufferToBase64,
|
||||
base64ToArrayBuffer,
|
||||
fromEncodedBinaryToArrayBuffer,
|
||||
} from '../Crypto';
|
||||
import dataInterface from '../sql/Client';
|
||||
import {
|
||||
AccountRecordClass,
|
||||
ContactRecordClass,
|
||||
GroupV1RecordClass,
|
||||
} from '../textsecure.d';
|
||||
import { ConversationModelType } from '../model-types.d';
|
||||
|
||||
const { updateConversation } = dataInterface;
|
||||
|
||||
type RecordClass = AccountRecordClass | ContactRecordClass | GroupV1RecordClass;
|
||||
|
||||
function toRecordVerified(verified: number): number {
|
||||
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
|
||||
const STATE_ENUM = window.textsecure.protobuf.ContactRecord.IdentityState;
|
||||
|
||||
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,
|
||||
conversation: ConversationModelType
|
||||
): void {
|
||||
if (record.__unknownFields) {
|
||||
conversation.set({
|
||||
storageUnknownFields: arrayBufferToBase64(record.__unknownFields),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function applyUnknownFields(
|
||||
record: RecordClass,
|
||||
conversation: ConversationModelType
|
||||
): void {
|
||||
if (conversation.get('storageUnknownFields')) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
record.__unknownFields = base64ToArrayBuffer(
|
||||
conversation.get('storageUnknownFields')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function toContactRecord(
|
||||
conversation: ConversationModelType
|
||||
): Promise<ContactRecordClass> {
|
||||
const contactRecord = new window.textsecure.protobuf.ContactRecord();
|
||||
if (conversation.get('uuid')) {
|
||||
contactRecord.serviceUuid = conversation.get('uuid');
|
||||
}
|
||||
if (conversation.get('e164')) {
|
||||
contactRecord.serviceE164 = conversation.get('e164');
|
||||
}
|
||||
if (conversation.get('profileKey')) {
|
||||
contactRecord.profileKey = base64ToArrayBuffer(
|
||||
String(conversation.get('profileKey'))
|
||||
);
|
||||
}
|
||||
const identityKey = await window.textsecure.storage.protocol.loadIdentityKey(
|
||||
conversation.id
|
||||
);
|
||||
if (identityKey) {
|
||||
contactRecord.identityKey = identityKey;
|
||||
}
|
||||
if (conversation.get('verified')) {
|
||||
contactRecord.identityState = toRecordVerified(
|
||||
Number(conversation.get('verified'))
|
||||
);
|
||||
}
|
||||
if (conversation.get('profileName')) {
|
||||
contactRecord.givenName = conversation.get('profileName');
|
||||
}
|
||||
if (conversation.get('profileFamilyName')) {
|
||||
contactRecord.familyName = conversation.get('profileFamilyName');
|
||||
}
|
||||
contactRecord.blocked = conversation.isBlocked();
|
||||
contactRecord.whitelisted = Boolean(conversation.get('profileSharing'));
|
||||
contactRecord.archived = Boolean(conversation.get('isArchived'));
|
||||
|
||||
applyUnknownFields(contactRecord, conversation);
|
||||
|
||||
return contactRecord;
|
||||
}
|
||||
|
||||
export async function toAccountRecord(
|
||||
conversation: ConversationModelType
|
||||
): Promise<AccountRecordClass> {
|
||||
const accountRecord = new window.textsecure.protobuf.AccountRecord();
|
||||
|
||||
if (conversation.get('profileKey')) {
|
||||
accountRecord.profileKey = base64ToArrayBuffer(
|
||||
String(conversation.get('profileKey'))
|
||||
);
|
||||
}
|
||||
if (conversation.get('profileName')) {
|
||||
accountRecord.givenName = conversation.get('profileName') || '';
|
||||
}
|
||||
if (conversation.get('profileFamilyName')) {
|
||||
accountRecord.familyName = conversation.get('profileFamilyName') || '';
|
||||
}
|
||||
accountRecord.avatarUrl = window.storage.get('avatarUrl') || '';
|
||||
accountRecord.noteToSelfArchived = Boolean(conversation.get('isArchived'));
|
||||
accountRecord.readReceipts = Boolean(
|
||||
window.storage.get('read-receipt-setting')
|
||||
);
|
||||
accountRecord.sealedSenderIndicators = Boolean(
|
||||
window.storage.get('sealedSenderIndicators')
|
||||
);
|
||||
accountRecord.typingIndicators = Boolean(
|
||||
window.storage.get('typingIndicators')
|
||||
);
|
||||
accountRecord.linkPreviews = Boolean(window.storage.get('linkPreviews'));
|
||||
|
||||
applyUnknownFields(accountRecord, conversation);
|
||||
|
||||
return accountRecord;
|
||||
}
|
||||
|
||||
export async function toGroupV1Record(
|
||||
conversation: ConversationModelType
|
||||
): Promise<GroupV1RecordClass> {
|
||||
const groupV1Record = new window.textsecure.protobuf.GroupV1Record();
|
||||
|
||||
groupV1Record.id = fromEncodedBinaryToArrayBuffer(
|
||||
String(conversation.get('groupId'))
|
||||
);
|
||||
groupV1Record.blocked = conversation.isBlocked();
|
||||
groupV1Record.whitelisted = Boolean(conversation.get('profileSharing'));
|
||||
groupV1Record.archived = Boolean(conversation.get('isArchived'));
|
||||
|
||||
applyUnknownFields(groupV1Record, conversation);
|
||||
|
||||
return groupV1Record;
|
||||
}
|
||||
|
||||
type MessageRequestCapableRecord = ContactRecordClass | GroupV1RecordClass;
|
||||
|
||||
function applyMessageRequestState(
|
||||
record: MessageRequestCapableRecord,
|
||||
conversation: ConversationModelType
|
||||
): void {
|
||||
if (record.blocked) {
|
||||
conversation.applyMessageRequestResponse(
|
||||
conversation.messageRequestEnum.BLOCK,
|
||||
{ fromSync: true, viaStorageServiceSync: true }
|
||||
);
|
||||
} 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
|
||||
conversation.applyMessageRequestResponse(
|
||||
conversation.messageRequestEnum.ACCEPT,
|
||||
{ fromSync: true, viaStorageServiceSync: true }
|
||||
);
|
||||
} 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) {
|
||||
conversation.disableProfileSharing({ viaStorageServiceSync: true });
|
||||
}
|
||||
}
|
||||
|
||||
function doesRecordHavePendingChanges(
|
||||
mergedRecord: RecordClass,
|
||||
serviceRecord: RecordClass,
|
||||
conversation: ConversationModelType
|
||||
): boolean {
|
||||
const shouldSync = Boolean(conversation.get('needsStorageServiceSync'));
|
||||
|
||||
const hasConflict = !_.isEqual(mergedRecord, serviceRecord);
|
||||
|
||||
if (shouldSync && !hasConflict) {
|
||||
conversation.set({ needsStorageServiceSync: false });
|
||||
}
|
||||
|
||||
return shouldSync && hasConflict;
|
||||
}
|
||||
|
||||
export async function mergeGroupV1Record(
|
||||
storageID: string,
|
||||
groupV1Record: GroupV1RecordClass
|
||||
): Promise<boolean> {
|
||||
window.log.info(`storageService.mergeGroupV1Record: merging ${storageID}`);
|
||||
|
||||
if (!groupV1Record.id) {
|
||||
window.log.info(
|
||||
`storageService.mergeGroupV1Record: no ID for ${storageID}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const groupId = groupV1Record.id.toBinary();
|
||||
|
||||
// We do a get here because we don't get enough information from just this source to
|
||||
// be able to do the right thing with this group. So we'll update the local group
|
||||
// record if we have one; otherwise we'll just drop this update.
|
||||
const conversation = window.ConversationController.get(groupId);
|
||||
if (!conversation) {
|
||||
window.log.warn(
|
||||
`storageService.mergeGroupV1Record: No conversation for group(${groupId})`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
conversation.set({
|
||||
isArchived: Boolean(groupV1Record.archived),
|
||||
storageID,
|
||||
});
|
||||
|
||||
applyMessageRequestState(groupV1Record, conversation);
|
||||
|
||||
addUnknownFields(groupV1Record, conversation);
|
||||
|
||||
const hasPendingChanges = doesRecordHavePendingChanges(
|
||||
await toGroupV1Record(conversation),
|
||||
groupV1Record,
|
||||
conversation
|
||||
);
|
||||
|
||||
updateConversation(conversation.attributes);
|
||||
|
||||
window.log.info(`storageService.mergeGroupV1Record: merged ${storageID}`);
|
||||
|
||||
return hasPendingChanges;
|
||||
}
|
||||
|
||||
export async function mergeContactRecord(
|
||||
storageID: string,
|
||||
contactRecord: ContactRecordClass
|
||||
): Promise<boolean> {
|
||||
window.log.info(`storageService.mergeContactRecord: merging ${storageID}`);
|
||||
|
||||
window.normalizeUuids(
|
||||
contactRecord,
|
||||
['serviceUuid'],
|
||||
'storageService.mergeContactRecord'
|
||||
);
|
||||
|
||||
const e164 = contactRecord.serviceE164 || undefined;
|
||||
const uuid = contactRecord.serviceUuid || undefined;
|
||||
|
||||
const id = window.ConversationController.ensureContactIds({
|
||||
e164,
|
||||
uuid,
|
||||
highTrust: true,
|
||||
});
|
||||
|
||||
if (!id) {
|
||||
window.log.info(
|
||||
`storageService.mergeContactRecord: no ID for ${storageID}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const conversation = await window.ConversationController.getOrCreateAndWait(
|
||||
id,
|
||||
'private'
|
||||
);
|
||||
|
||||
if (contactRecord.profileKey) {
|
||||
await conversation.setProfileKey(
|
||||
arrayBufferToBase64(contactRecord.profileKey.toArrayBuffer()),
|
||||
{ viaStorageServiceSync: true }
|
||||
);
|
||||
}
|
||||
|
||||
const verified = await conversation.safeGetVerified();
|
||||
const storageServiceVerified = contactRecord.identityState || 0;
|
||||
if (verified !== storageServiceVerified) {
|
||||
const verifiedOptions = { viaStorageServiceSync: true };
|
||||
const STATE_ENUM = window.textsecure.protobuf.ContactRecord.IdentityState;
|
||||
|
||||
switch (storageServiceVerified) {
|
||||
case STATE_ENUM.VERIFIED:
|
||||
await conversation.setVerified(verifiedOptions);
|
||||
break;
|
||||
case STATE_ENUM.UNVERIFIED:
|
||||
await conversation.setUnverified(verifiedOptions);
|
||||
break;
|
||||
default:
|
||||
await conversation.setVerifiedDefault(verifiedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
applyMessageRequestState(contactRecord, conversation);
|
||||
|
||||
addUnknownFields(contactRecord, conversation);
|
||||
|
||||
conversation.set({
|
||||
isArchived: Boolean(contactRecord.archived),
|
||||
storageID,
|
||||
});
|
||||
|
||||
const hasPendingChanges = doesRecordHavePendingChanges(
|
||||
await toContactRecord(conversation),
|
||||
contactRecord,
|
||||
conversation
|
||||
);
|
||||
|
||||
updateConversation(conversation.attributes);
|
||||
|
||||
window.log.info(`storageService.mergeContactRecord: merged ${storageID}`);
|
||||
|
||||
return hasPendingChanges;
|
||||
}
|
||||
|
||||
export async function mergeAccountRecord(
|
||||
storageID: string,
|
||||
accountRecord: AccountRecordClass
|
||||
): Promise<boolean> {
|
||||
window.log.info(`storageService.mergeAccountRecord: merging ${storageID}`);
|
||||
|
||||
const {
|
||||
avatarUrl,
|
||||
linkPreviews,
|
||||
noteToSelfArchived,
|
||||
profileKey,
|
||||
readReceipts,
|
||||
sealedSenderIndicators,
|
||||
typingIndicators,
|
||||
} = accountRecord;
|
||||
|
||||
window.storage.put('read-receipt-setting', readReceipts);
|
||||
|
||||
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 (profileKey) {
|
||||
window.storage.put('profileKey', profileKey.toArrayBuffer());
|
||||
}
|
||||
|
||||
window.log.info(
|
||||
`storageService.mergeAccountRecord: merged settings ${storageID}`
|
||||
);
|
||||
|
||||
const ourID = window.ConversationController.getOurConversationId();
|
||||
|
||||
if (!ourID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const conversation = await window.ConversationController.getOrCreateAndWait(
|
||||
ourID,
|
||||
'private'
|
||||
);
|
||||
|
||||
addUnknownFields(accountRecord, conversation);
|
||||
|
||||
conversation.set({
|
||||
isArchived: Boolean(noteToSelfArchived),
|
||||
storageID,
|
||||
});
|
||||
|
||||
if (accountRecord.profileKey) {
|
||||
await conversation.setProfileKey(
|
||||
arrayBufferToBase64(accountRecord.profileKey.toArrayBuffer())
|
||||
);
|
||||
}
|
||||
|
||||
if (avatarUrl) {
|
||||
await conversation.setProfileAvatar(avatarUrl);
|
||||
window.storage.put('avatarUrl', avatarUrl);
|
||||
}
|
||||
|
||||
const hasPendingChanges = doesRecordHavePendingChanges(
|
||||
await toAccountRecord(conversation),
|
||||
accountRecord,
|
||||
conversation
|
||||
);
|
||||
|
||||
updateConversation(conversation.attributes);
|
||||
|
||||
window.log.info(
|
||||
`storageService.mergeAccountRecord: merged profile ${storageID}`
|
||||
);
|
||||
|
||||
return hasPendingChanges;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue