Use storage service for call links
This commit is contained in:
parent
50447b7686
commit
5a75246e42
23 changed files with 583 additions and 50 deletions
|
@ -622,7 +622,7 @@ message SyncMessage {
|
||||||
message CallLinkUpdate {
|
message CallLinkUpdate {
|
||||||
enum Type {
|
enum Type {
|
||||||
UPDATE = 0;
|
UPDATE = 0;
|
||||||
DELETE = 1;
|
reserved 1; // was DELETE, superseded by storage service
|
||||||
}
|
}
|
||||||
|
|
||||||
optional bytes rootKey = 1;
|
optional bytes rootKey = 1;
|
||||||
|
|
|
@ -47,6 +47,7 @@ message ManifestRecord {
|
||||||
ACCOUNT = 4;
|
ACCOUNT = 4;
|
||||||
STORY_DISTRIBUTION_LIST = 5;
|
STORY_DISTRIBUTION_LIST = 5;
|
||||||
STICKER_PACK = 6;
|
STICKER_PACK = 6;
|
||||||
|
CALL_LINK = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional bytes raw = 1;
|
optional bytes raw = 1;
|
||||||
|
@ -67,6 +68,7 @@ message StorageRecord {
|
||||||
AccountRecord account = 4;
|
AccountRecord account = 4;
|
||||||
StoryDistributionListRecord storyDistributionList = 5;
|
StoryDistributionListRecord storyDistributionList = 5;
|
||||||
StickerPackRecord stickerPack = 6;
|
StickerPackRecord stickerPack = 6;
|
||||||
|
CallLinkRecord callLink = 7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,3 +243,10 @@ message StickerPackRecord {
|
||||||
// non-zero - `packKey` and `position` should
|
// non-zero - `packKey` and `position` should
|
||||||
// be unset
|
// be unset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CallLinkRecord {
|
||||||
|
optional bytes rootKey = 1; // 16 bytes
|
||||||
|
optional bytes adminPasskey = 2; // Non-empty when the current user is an admin
|
||||||
|
optional uint64 deletedAtTimestampMs = 3; // When present and non-zero, `adminPasskey`
|
||||||
|
// should be cleared
|
||||||
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ function getCallLink(overrideProps: Partial<CallLinkType> = {}): CallLinkType {
|
||||||
restrictions: CallLinkRestrictions.None,
|
restrictions: CallLinkRestrictions.None,
|
||||||
revoked: false,
|
revoked: false,
|
||||||
expiration: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
expiration: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
||||||
|
storageNeedsSync: false,
|
||||||
...overrideProps,
|
...overrideProps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1074,6 +1074,7 @@ export class BackupImportStream extends Writable {
|
||||||
restrictions: fromCallLinkRestrictionsProto(restrictions),
|
restrictions: fromCallLinkRestrictionsProto(restrictions),
|
||||||
revoked: false,
|
revoked: false,
|
||||||
expiration: expirationMs?.toNumber() || null,
|
expiration: expirationMs?.toNumber() || null,
|
||||||
|
storageNeedsSync: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.recipientIdToCallLink.set(recipientId, callLink);
|
this.recipientIdToCallLink.set(recipientId, callLink);
|
||||||
|
|
|
@ -153,10 +153,7 @@ import {
|
||||||
import type { CallLinkType, CallLinkStateType } from '../types/CallLink';
|
import type { CallLinkType, CallLinkStateType } from '../types/CallLink';
|
||||||
import { CallLinkRestrictions } from '../types/CallLink';
|
import { CallLinkRestrictions } from '../types/CallLink';
|
||||||
import { getConversationIdForLogging } from '../util/idForLogging';
|
import { getConversationIdForLogging } from '../util/idForLogging';
|
||||||
import {
|
import { sendCallLinkUpdateSync } from '../util/sendCallLinkUpdateSync';
|
||||||
sendCallLinkDeleteSync,
|
|
||||||
sendCallLinkUpdateSync,
|
|
||||||
} from '../util/sendCallLinkUpdateSync';
|
|
||||||
import { createIdenticon } from '../util/createIdenticon';
|
import { createIdenticon } from '../util/createIdenticon';
|
||||||
import { getColorForCallLink } from '../util/getColorForCallLink';
|
import { getColorForCallLink } from '../util/getColorForCallLink';
|
||||||
|
|
||||||
|
@ -677,6 +674,7 @@ export class CallingClass {
|
||||||
roomId: roomIdHex,
|
roomId: roomIdHex,
|
||||||
rootKey: rootKey.toString(),
|
rootKey: rootKey.toString(),
|
||||||
adminKey: adminKey.toString('base64'),
|
adminKey: adminKey.toString('base64'),
|
||||||
|
storageNeedsSync: true,
|
||||||
...state,
|
...state,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -716,8 +714,6 @@ export class CallingClass {
|
||||||
log.error(`${logId}: ${message}`);
|
log.error(`${logId}: ${message}`);
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(sendCallLinkDeleteSync(callLink));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCallLinkName(
|
async updateCallLinkName(
|
||||||
|
|
|
@ -29,6 +29,8 @@ import {
|
||||||
toGroupV2Record,
|
toGroupV2Record,
|
||||||
toStoryDistributionListRecord,
|
toStoryDistributionListRecord,
|
||||||
toStickerPackRecord,
|
toStickerPackRecord,
|
||||||
|
toCallLinkRecord,
|
||||||
|
mergeCallLinkRecord,
|
||||||
} from './storageRecordOps';
|
} from './storageRecordOps';
|
||||||
import type { MergeResultType } from './storageRecordOps';
|
import type { MergeResultType } from './storageRecordOps';
|
||||||
import { MAX_READ_KEYS } from './storageConstants';
|
import { MAX_READ_KEYS } from './storageConstants';
|
||||||
|
@ -68,6 +70,8 @@ import { MY_STORY_ID } from '../types/Stories';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
import { isNotNil } from '../util/isNotNil';
|
||||||
import { isSignalConversation } from '../util/isSignalConversation';
|
import { isSignalConversation } from '../util/isSignalConversation';
|
||||||
import { redactExtendedStorageID, redactStorageID } from '../util/privacy';
|
import { redactExtendedStorageID, redactStorageID } from '../util/privacy';
|
||||||
|
import type { CallLinkRecord } from '../types/CallLink';
|
||||||
|
import { callLinkFromRecord } from '../util/callLinksRingrtc';
|
||||||
|
|
||||||
type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier;
|
type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier;
|
||||||
|
|
||||||
|
@ -90,6 +94,7 @@ const validRecordTypes = new Set([
|
||||||
4, // ACCOUNT
|
4, // ACCOUNT
|
||||||
5, // STORY_DISTRIBUTION_LIST
|
5, // STORY_DISTRIBUTION_LIST
|
||||||
6, // STICKER_PACK
|
6, // STICKER_PACK
|
||||||
|
7, // CALL_LINK
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const backOff = new BackOff([
|
const backOff = new BackOff([
|
||||||
|
@ -326,6 +331,7 @@ async function generateManifest(
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
callLinkDbRecords,
|
||||||
storyDistributionLists,
|
storyDistributionLists,
|
||||||
installedStickerPacks,
|
installedStickerPacks,
|
||||||
uninstalledStickerPacks,
|
uninstalledStickerPacks,
|
||||||
|
@ -460,6 +466,57 @@ async function generateManifest(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`storageService.upload(${version}): ` +
|
||||||
|
`adding callLinks=${callLinkDbRecords.length}`
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const callLinkDbRecord of callLinkDbRecords) {
|
||||||
|
const { roomId } = callLinkDbRecord;
|
||||||
|
if (callLinkDbRecord.adminKey == null || callLinkDbRecord.rootKey == null) {
|
||||||
|
log.warn(
|
||||||
|
`storageService.upload(${version}): ` +
|
||||||
|
`call link ${roomId} has empty rootKey`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageRecord = new Proto.StorageRecord();
|
||||||
|
storageRecord.callLink = toCallLinkRecord(callLinkDbRecord);
|
||||||
|
|
||||||
|
const callLink = callLinkFromRecord(callLinkDbRecord);
|
||||||
|
const { isNewItem, storageID } = processStorageRecord({
|
||||||
|
currentStorageID: callLink.storageID,
|
||||||
|
currentStorageVersion: callLink.storageVersion,
|
||||||
|
identifierType: ITEM_TYPE.CALL_LINK,
|
||||||
|
storageNeedsSync: callLink.storageNeedsSync,
|
||||||
|
storageRecord,
|
||||||
|
});
|
||||||
|
|
||||||
|
const storageFields = {
|
||||||
|
storageID,
|
||||||
|
storageVersion: version,
|
||||||
|
storageNeedsSync: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isNewItem) {
|
||||||
|
postUploadUpdateFunctions.push(async () => {
|
||||||
|
const freshCallLink = await DataReader.getCallLinkByRoomId(roomId);
|
||||||
|
if (freshCallLink == null) {
|
||||||
|
log.warn(
|
||||||
|
`storageService.upload(${version}): ` +
|
||||||
|
`call link ${roomId} removed locally from DB while we were uploading to storage`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const callLinkToSave = { ...freshCallLink, ...storageFields };
|
||||||
|
await DataWriter.updateCallLink(callLinkToSave);
|
||||||
|
window.reduxActions.calling.handleCallLinkUpdateLocal(callLinkToSave);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const unknownRecordsArray: ReadonlyArray<UnknownRecord> = (
|
const unknownRecordsArray: ReadonlyArray<UnknownRecord> = (
|
||||||
window.storage.get('storage-service-unknown-records') || []
|
window.storage.get('storage-service-unknown-records') || []
|
||||||
).filter((record: UnknownRecord) => !validRecordTypes.has(record.itemType));
|
).filter((record: UnknownRecord) => !validRecordTypes.has(record.itemType));
|
||||||
|
@ -1023,6 +1080,12 @@ async function mergeRecord(
|
||||||
storageVersion,
|
storageVersion,
|
||||||
storageRecord.stickerPack
|
storageRecord.stickerPack
|
||||||
);
|
);
|
||||||
|
} else if (itemType === ITEM_TYPE.CALL_LINK && storageRecord.callLink) {
|
||||||
|
mergeResult = await mergeCallLinkRecord(
|
||||||
|
storageID,
|
||||||
|
storageVersion,
|
||||||
|
storageRecord.callLink
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
isUnsupported = true;
|
isUnsupported = true;
|
||||||
log.warn(
|
log.warn(
|
||||||
|
@ -1077,6 +1140,7 @@ async function mergeRecord(
|
||||||
}
|
}
|
||||||
|
|
||||||
type NonConversationRecordsResultType = Readonly<{
|
type NonConversationRecordsResultType = Readonly<{
|
||||||
|
callLinkDbRecords: ReadonlyArray<CallLinkRecord>;
|
||||||
installedStickerPacks: ReadonlyArray<StickerPackType>;
|
installedStickerPacks: ReadonlyArray<StickerPackType>;
|
||||||
uninstalledStickerPacks: ReadonlyArray<UninstalledStickerPackType>;
|
uninstalledStickerPacks: ReadonlyArray<UninstalledStickerPackType>;
|
||||||
storyDistributionLists: ReadonlyArray<StoryDistributionWithMembersType>;
|
storyDistributionLists: ReadonlyArray<StoryDistributionWithMembersType>;
|
||||||
|
@ -1085,16 +1149,19 @@ type NonConversationRecordsResultType = Readonly<{
|
||||||
// TODO: DESKTOP-3929
|
// TODO: DESKTOP-3929
|
||||||
async function getNonConversationRecords(): Promise<NonConversationRecordsResultType> {
|
async function getNonConversationRecords(): Promise<NonConversationRecordsResultType> {
|
||||||
const [
|
const [
|
||||||
|
callLinkDbRecords,
|
||||||
storyDistributionLists,
|
storyDistributionLists,
|
||||||
uninstalledStickerPacks,
|
uninstalledStickerPacks,
|
||||||
installedStickerPacks,
|
installedStickerPacks,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
|
DataReader.getAllCallLinkRecordsWithAdminKey(),
|
||||||
DataReader.getAllStoryDistributionsWithMembers(),
|
DataReader.getAllStoryDistributionsWithMembers(),
|
||||||
DataReader.getUninstalledStickerPacks(),
|
DataReader.getUninstalledStickerPacks(),
|
||||||
DataReader.getInstalledStickerPacks(),
|
DataReader.getInstalledStickerPacks(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
callLinkDbRecords,
|
||||||
storyDistributionLists,
|
storyDistributionLists,
|
||||||
uninstalledStickerPacks,
|
uninstalledStickerPacks,
|
||||||
installedStickerPacks,
|
installedStickerPacks,
|
||||||
|
@ -1130,6 +1197,7 @@ async function processManifest(
|
||||||
|
|
||||||
{
|
{
|
||||||
const {
|
const {
|
||||||
|
callLinkDbRecords,
|
||||||
storyDistributionLists,
|
storyDistributionLists,
|
||||||
installedStickerPacks,
|
installedStickerPacks,
|
||||||
uninstalledStickerPacks,
|
uninstalledStickerPacks,
|
||||||
|
@ -1144,6 +1212,11 @@ async function processManifest(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
callLinkDbRecords.forEach(dbRecord =>
|
||||||
|
collectLocalKeysFromFields(callLinkFromRecord(dbRecord))
|
||||||
|
);
|
||||||
|
localRecordCount += callLinkDbRecords.length;
|
||||||
|
|
||||||
storyDistributionLists.forEach(collectLocalKeysFromFields);
|
storyDistributionLists.forEach(collectLocalKeysFromFields);
|
||||||
localRecordCount += storyDistributionLists.length;
|
localRecordCount += storyDistributionLists.length;
|
||||||
|
|
||||||
|
@ -1264,6 +1337,7 @@ async function processManifest(
|
||||||
// Refetch various records post-merge
|
// Refetch various records post-merge
|
||||||
{
|
{
|
||||||
const {
|
const {
|
||||||
|
callLinkDbRecords,
|
||||||
storyDistributionLists,
|
storyDistributionLists,
|
||||||
installedStickerPacks,
|
installedStickerPacks,
|
||||||
uninstalledStickerPacks,
|
uninstalledStickerPacks,
|
||||||
|
@ -1352,6 +1426,30 @@ async function processManifest(
|
||||||
|
|
||||||
conflictCount += 1;
|
conflictCount += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callLinkDbRecords.forEach(callLinkDbRecord => {
|
||||||
|
const { storageID, storageVersion } = callLinkDbRecord;
|
||||||
|
if (!storageID || remoteKeys.has(storageID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingKey = redactStorageID(
|
||||||
|
storageID,
|
||||||
|
storageVersion || undefined
|
||||||
|
);
|
||||||
|
log.info(
|
||||||
|
`storageService.process(${version}): localKey=${missingKey} was not ` +
|
||||||
|
'in remote manifest'
|
||||||
|
);
|
||||||
|
const callLink = callLinkFromRecord(callLinkDbRecord);
|
||||||
|
drop(
|
||||||
|
DataWriter.updateCallLink({
|
||||||
|
...callLink,
|
||||||
|
storageID: undefined,
|
||||||
|
storageVersion: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { isEqual, isNumber } from 'lodash';
|
import { isEqual, isNumber } from 'lodash';
|
||||||
import Long from 'long';
|
import Long from 'long';
|
||||||
|
|
||||||
|
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||||
import { uuidToBytes, bytesToUuid } from '../util/uuidToBytes';
|
import { uuidToBytes, bytesToUuid } from '../util/uuidToBytes';
|
||||||
import { deriveMasterKeyFromGroupV1 } from '../Crypto';
|
import { deriveMasterKeyFromGroupV1 } from '../Crypto';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
|
@ -65,6 +66,18 @@ import { findAndDeleteOnboardingStoryIfExists } from '../util/findAndDeleteOnboa
|
||||||
import { downloadOnboardingStory } from '../util/downloadOnboardingStory';
|
import { downloadOnboardingStory } from '../util/downloadOnboardingStory';
|
||||||
import { drop } from '../util/drop';
|
import { drop } from '../util/drop';
|
||||||
import { redactExtendedStorageID } from '../util/privacy';
|
import { redactExtendedStorageID } from '../util/privacy';
|
||||||
|
import type { CallLinkRecord } from '../types/CallLink';
|
||||||
|
import {
|
||||||
|
callLinkFromRecord,
|
||||||
|
fromRootKeyBytes,
|
||||||
|
getRoomIdFromRootKey,
|
||||||
|
} from '../util/callLinksRingrtc';
|
||||||
|
import {
|
||||||
|
CALL_LINK_DELETED_STORAGE_RECORD_TTL,
|
||||||
|
fromAdminKeyBytes,
|
||||||
|
toCallHistoryFromUnusedCallLink,
|
||||||
|
} from '../util/callLinks';
|
||||||
|
import { isOlderThan } from '../util/timestamp';
|
||||||
|
|
||||||
const MY_STORY_BYTES = uuidToBytes(MY_STORY_ID);
|
const MY_STORY_BYTES = uuidToBytes(MY_STORY_ID);
|
||||||
|
|
||||||
|
@ -603,6 +616,35 @@ export function toStickerPackRecord(
|
||||||
return stickerPackRecord;
|
return stickerPackRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// callLinkDbRecord exposes additional fields not available on CallLinkType
|
||||||
|
export function toCallLinkRecord(
|
||||||
|
callLinkDbRecord: CallLinkRecord
|
||||||
|
): Proto.CallLinkRecord {
|
||||||
|
strictAssert(callLinkDbRecord.rootKey, 'toCallLinkRecord: no rootKey');
|
||||||
|
|
||||||
|
const callLinkRecord = new Proto.CallLinkRecord();
|
||||||
|
|
||||||
|
callLinkRecord.rootKey = callLinkDbRecord.rootKey;
|
||||||
|
if (callLinkDbRecord.deleted === 1) {
|
||||||
|
// adminKey is intentionally omitted for deleted call links.
|
||||||
|
callLinkRecord.deletedAtTimestampMs = Long.fromNumber(
|
||||||
|
callLinkDbRecord.deletedAt || new Date().getTime()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
strictAssert(
|
||||||
|
callLinkDbRecord.adminKey,
|
||||||
|
'toCallLinkRecord: no adminPasskey'
|
||||||
|
);
|
||||||
|
callLinkRecord.adminPasskey = callLinkDbRecord.adminKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callLinkDbRecord.storageUnknownFields) {
|
||||||
|
callLinkRecord.$unknownFields = [callLinkDbRecord.storageUnknownFields];
|
||||||
|
}
|
||||||
|
|
||||||
|
return callLinkRecord;
|
||||||
|
}
|
||||||
|
|
||||||
type MessageRequestCapableRecord = Proto.IContactRecord | Proto.IGroupV1Record;
|
type MessageRequestCapableRecord = Proto.IContactRecord | Proto.IGroupV1Record;
|
||||||
|
|
||||||
function applyMessageRequestState(
|
function applyMessageRequestState(
|
||||||
|
@ -1906,3 +1948,152 @@ export async function mergeStickerPackRecord(
|
||||||
oldStorageVersion,
|
oldStorageVersion,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function mergeCallLinkRecord(
|
||||||
|
storageID: string,
|
||||||
|
storageVersion: number,
|
||||||
|
callLinkRecord: Proto.ICallLinkRecord
|
||||||
|
): Promise<MergeResultType> {
|
||||||
|
const redactedStorageID = redactExtendedStorageID({
|
||||||
|
storageID,
|
||||||
|
storageVersion,
|
||||||
|
});
|
||||||
|
// callLinkRecords must have rootKey
|
||||||
|
if (!callLinkRecord.rootKey) {
|
||||||
|
return { hasConflict: false, shouldDrop: true, details: ['no rootKey'] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const details: Array<string> = [];
|
||||||
|
|
||||||
|
const rootKeyString = fromRootKeyBytes(callLinkRecord.rootKey);
|
||||||
|
const adminKeyString = callLinkRecord.adminPasskey
|
||||||
|
? fromAdminKeyBytes(callLinkRecord.adminPasskey)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const callLinkRootKey = CallLinkRootKey.parse(rootKeyString);
|
||||||
|
const roomId = getRoomIdFromRootKey(callLinkRootKey);
|
||||||
|
const logId = `mergeCallLinkRecord(${redactedStorageID}, ${roomId})`;
|
||||||
|
|
||||||
|
const localCallLinkDbRecord =
|
||||||
|
await DataReader.getCallLinkRecordByRoomId(roomId);
|
||||||
|
|
||||||
|
const deletedAt: number | null =
|
||||||
|
callLinkRecord.deletedAtTimestampMs != null
|
||||||
|
? getTimestampFromLong(callLinkRecord.deletedAtTimestampMs)
|
||||||
|
: null;
|
||||||
|
const shouldDrop =
|
||||||
|
deletedAt != null &&
|
||||||
|
isOlderThan(deletedAt, CALL_LINK_DELETED_STORAGE_RECORD_TTL);
|
||||||
|
if (shouldDrop) {
|
||||||
|
details.push('expired deleted call link; scheduling for removal');
|
||||||
|
}
|
||||||
|
|
||||||
|
const callLinkDbRecord: CallLinkRecord = {
|
||||||
|
roomId,
|
||||||
|
rootKey: callLinkRecord.rootKey,
|
||||||
|
adminKey: callLinkRecord.adminPasskey ?? null,
|
||||||
|
name: localCallLinkDbRecord?.name ?? '',
|
||||||
|
restrictions: localCallLinkDbRecord?.restrictions ?? 0,
|
||||||
|
expiration: localCallLinkDbRecord?.expiration ?? null,
|
||||||
|
revoked: localCallLinkDbRecord?.revoked === 1 ? 1 : 0,
|
||||||
|
deleted: deletedAt ? 1 : 0,
|
||||||
|
deletedAt,
|
||||||
|
|
||||||
|
storageID,
|
||||||
|
storageVersion,
|
||||||
|
storageUnknownFields: callLinkRecord.$unknownFields
|
||||||
|
? Bytes.concatenate(callLinkRecord.$unknownFields)
|
||||||
|
: null,
|
||||||
|
storageNeedsSync: localCallLinkDbRecord?.storageNeedsSync === 1 ? 1 : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!localCallLinkDbRecord) {
|
||||||
|
if (deletedAt) {
|
||||||
|
log.info(
|
||||||
|
`${logId}: Found deleted call link with no matching local record, skipping`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.info(`${logId}: Discovered new call link, creating locally`);
|
||||||
|
details.push('creating call link');
|
||||||
|
|
||||||
|
// Create CallLink and call history item
|
||||||
|
const callLink = callLinkFromRecord(callLinkDbRecord);
|
||||||
|
const callHistory = toCallHistoryFromUnusedCallLink(callLink);
|
||||||
|
await Promise.all([
|
||||||
|
DataWriter.insertCallLink(callLink),
|
||||||
|
DataWriter.saveCallHistory(callHistory),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Refresh call link state via RingRTC and update in redux
|
||||||
|
window.reduxActions.calling.handleCallLinkUpdate({
|
||||||
|
rootKey: rootKeyString,
|
||||||
|
adminKey: adminKeyString,
|
||||||
|
});
|
||||||
|
window.reduxActions.callHistory.addCallHistory(callHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
details,
|
||||||
|
hasConflict: false,
|
||||||
|
shouldDrop,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldStorageID = localCallLinkDbRecord.storageID || undefined;
|
||||||
|
const oldStorageVersion = localCallLinkDbRecord.storageVersion || undefined;
|
||||||
|
|
||||||
|
const needsToClearUnknownFields =
|
||||||
|
!callLinkRecord.$unknownFields &&
|
||||||
|
localCallLinkDbRecord.storageUnknownFields;
|
||||||
|
if (needsToClearUnknownFields) {
|
||||||
|
details.push('clearing unknown fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBadRemoteData = Boolean(deletedAt && adminKeyString);
|
||||||
|
if (isBadRemoteData) {
|
||||||
|
log.warn(
|
||||||
|
`${logId}: Found bad remote data: deletedAtTimestampMs and adminPasskey were both present. Assuming deleted.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hasConflict, details: conflictDetails } = doRecordsConflict(
|
||||||
|
toCallLinkRecord(callLinkDbRecord),
|
||||||
|
callLinkRecord
|
||||||
|
);
|
||||||
|
|
||||||
|
// First update local record
|
||||||
|
details.push('updated');
|
||||||
|
const callLink = callLinkFromRecord(callLinkDbRecord);
|
||||||
|
await DataWriter.updateCallLink(callLink);
|
||||||
|
|
||||||
|
// Deleted in storage but we have it locally: Delete locally too and update redux
|
||||||
|
if (deletedAt && localCallLinkDbRecord.deleted !== 1) {
|
||||||
|
// Another device deleted the link and uploaded to storage, and we learned about it
|
||||||
|
log.info(`${logId}: Discovered deleted call link, deleting locally`);
|
||||||
|
details.push('deleting locally');
|
||||||
|
await DataWriter.beginDeleteCallLink(roomId, {
|
||||||
|
storageNeedsSync: false,
|
||||||
|
deletedAt,
|
||||||
|
});
|
||||||
|
// No need to delete via RingRTC as we assume the originating device did that already
|
||||||
|
await DataWriter.finalizeDeleteCallLink(roomId);
|
||||||
|
window.reduxActions.calling.handleCallLinkDelete({ roomId });
|
||||||
|
} else if (!deletedAt && localCallLinkDbRecord.deleted === 1) {
|
||||||
|
// Not deleted in storage, but we've marked it as deleted locally.
|
||||||
|
// Skip doing anything, we will update things locally after sync.
|
||||||
|
log.warn(`${logId}: Found call link, but it was marked deleted locally.`);
|
||||||
|
} else {
|
||||||
|
window.reduxActions.calling.handleCallLinkUpdate({
|
||||||
|
rootKey: rootKeyString,
|
||||||
|
adminKey: adminKeyString,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
details: [...details, ...conflictDetails],
|
||||||
|
hasConflict,
|
||||||
|
shouldDrop,
|
||||||
|
oldStorageID,
|
||||||
|
oldStorageVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -31,12 +31,17 @@ import type {
|
||||||
CallHistoryPagination,
|
CallHistoryPagination,
|
||||||
CallLogEventTarget,
|
CallLogEventTarget,
|
||||||
} from '../types/CallDisposition';
|
} from '../types/CallDisposition';
|
||||||
import type { CallLinkStateType, CallLinkType } from '../types/CallLink';
|
import type {
|
||||||
|
CallLinkRecord,
|
||||||
|
CallLinkStateType,
|
||||||
|
CallLinkType,
|
||||||
|
} from '../types/CallLink';
|
||||||
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
|
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
|
||||||
import type { GroupSendEndorsementsData } from '../types/GroupSendEndorsements';
|
import type { GroupSendEndorsementsData } from '../types/GroupSendEndorsements';
|
||||||
import type { SyncTaskType } from '../util/syncTasks';
|
import type { SyncTaskType } from '../util/syncTasks';
|
||||||
import type { AttachmentBackupJobType } from '../types/AttachmentBackup';
|
import type { AttachmentBackupJobType } from '../types/AttachmentBackup';
|
||||||
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||||
|
import type { DeleteCallLinkOptions } from './server/callLinks';
|
||||||
|
|
||||||
export type ReadableDB = Database & { __readable_db: never };
|
export type ReadableDB = Database & { __readable_db: never };
|
||||||
export type WritableDB = ReadableDB & { __writable_db: never };
|
export type WritableDB = ReadableDB & { __writable_db: never };
|
||||||
|
@ -568,6 +573,8 @@ type ReadableInterface = {
|
||||||
callLinkExists(roomId: string): boolean;
|
callLinkExists(roomId: string): boolean;
|
||||||
getAllCallLinks: () => ReadonlyArray<CallLinkType>;
|
getAllCallLinks: () => ReadonlyArray<CallLinkType>;
|
||||||
getCallLinkByRoomId: (roomId: string) => CallLinkType | undefined;
|
getCallLinkByRoomId: (roomId: string) => CallLinkType | undefined;
|
||||||
|
getCallLinkRecordByRoomId: (roomId: string) => CallLinkRecord | undefined;
|
||||||
|
getAllCallLinkRecordsWithAdminKey(): ReadonlyArray<CallLinkRecord>;
|
||||||
getAllMarkedDeletedCallLinks(): ReadonlyArray<CallLinkType>;
|
getAllMarkedDeletedCallLinks(): ReadonlyArray<CallLinkType>;
|
||||||
getMessagesBetween: (
|
getMessagesBetween: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
|
@ -799,13 +806,14 @@ type WritableInterface = {
|
||||||
markCallHistoryMissed(callIds: ReadonlyArray<string>): void;
|
markCallHistoryMissed(callIds: ReadonlyArray<string>): void;
|
||||||
getRecentStaleRingsAndMarkOlderMissed(): ReadonlyArray<MaybeStaleCallHistory>;
|
getRecentStaleRingsAndMarkOlderMissed(): ReadonlyArray<MaybeStaleCallHistory>;
|
||||||
insertCallLink(callLink: CallLinkType): void;
|
insertCallLink(callLink: CallLinkType): void;
|
||||||
|
updateCallLink(callLink: CallLinkType): void;
|
||||||
updateCallLinkAdminKeyByRoomId(roomId: string, adminKey: string): void;
|
updateCallLinkAdminKeyByRoomId(roomId: string, adminKey: string): void;
|
||||||
updateCallLinkState(
|
updateCallLinkState(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
callLinkState: CallLinkStateType
|
callLinkState: CallLinkStateType
|
||||||
): CallLinkType;
|
): CallLinkType;
|
||||||
beginDeleteAllCallLinks(): void;
|
beginDeleteAllCallLinks(): void;
|
||||||
beginDeleteCallLink(roomId: string): void;
|
beginDeleteCallLink(roomId: string, options: DeleteCallLinkOptions): void;
|
||||||
finalizeDeleteCallLink(roomId: string): void;
|
finalizeDeleteCallLink(roomId: string): void;
|
||||||
_removeAllCallLinks(): void;
|
_removeAllCallLinks(): void;
|
||||||
deleteCallLinkFromSync(roomId: string): void;
|
deleteCallLinkFromSync(roomId: string): void;
|
||||||
|
|
|
@ -174,10 +174,13 @@ import {
|
||||||
callLinkExists,
|
callLinkExists,
|
||||||
getAllCallLinks,
|
getAllCallLinks,
|
||||||
getCallLinkByRoomId,
|
getCallLinkByRoomId,
|
||||||
|
getCallLinkRecordByRoomId,
|
||||||
insertCallLink,
|
insertCallLink,
|
||||||
|
updateCallLink,
|
||||||
updateCallLinkAdminKeyByRoomId,
|
updateCallLinkAdminKeyByRoomId,
|
||||||
updateCallLinkState,
|
updateCallLinkState,
|
||||||
beginDeleteAllCallLinks,
|
beginDeleteAllCallLinks,
|
||||||
|
getAllCallLinkRecordsWithAdminKey,
|
||||||
getAllMarkedDeletedCallLinks,
|
getAllMarkedDeletedCallLinks,
|
||||||
finalizeDeleteCallLink,
|
finalizeDeleteCallLink,
|
||||||
beginDeleteCallLink,
|
beginDeleteCallLink,
|
||||||
|
@ -304,6 +307,8 @@ export const DataReader: ServerReadableInterface = {
|
||||||
callLinkExists,
|
callLinkExists,
|
||||||
getAllCallLinks,
|
getAllCallLinks,
|
||||||
getCallLinkByRoomId,
|
getCallLinkByRoomId,
|
||||||
|
getCallLinkRecordByRoomId,
|
||||||
|
getAllCallLinkRecordsWithAdminKey,
|
||||||
getAllMarkedDeletedCallLinks,
|
getAllMarkedDeletedCallLinks,
|
||||||
getMessagesBetween,
|
getMessagesBetween,
|
||||||
getNearbyMessageFromDeletedSet,
|
getNearbyMessageFromDeletedSet,
|
||||||
|
@ -439,6 +444,7 @@ export const DataWriter: ServerWritableInterface = {
|
||||||
saveCallHistory,
|
saveCallHistory,
|
||||||
markCallHistoryMissed,
|
markCallHistoryMissed,
|
||||||
insertCallLink,
|
insertCallLink,
|
||||||
|
updateCallLink,
|
||||||
updateCallLinkAdminKeyByRoomId,
|
updateCallLinkAdminKeyByRoomId,
|
||||||
updateCallLinkState,
|
updateCallLinkState,
|
||||||
beginDeleteAllCallLinks,
|
beginDeleteAllCallLinks,
|
||||||
|
@ -6445,6 +6451,14 @@ function eraseStorageServiceState(db: WritableDB): void {
|
||||||
storageVersion = null,
|
storageVersion = null,
|
||||||
storageUnknownFields = null,
|
storageUnknownFields = null,
|
||||||
storageNeedsSync = 0;
|
storageNeedsSync = 0;
|
||||||
|
|
||||||
|
-- Call links
|
||||||
|
UPDATE callLinks
|
||||||
|
SET
|
||||||
|
storageID = null,
|
||||||
|
storageVersion = null,
|
||||||
|
storageUnknownFields = null,
|
||||||
|
storageNeedsSync = 0;
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
38
ts/sql/migrations/1190-call-links-storage.ts
Normal file
38
ts/sql/migrations/1190-call-links-storage.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
import type { Database } from '@signalapp/better-sqlite3';
|
||||||
|
import type { LoggerType } from '../../types/Logging';
|
||||||
|
|
||||||
|
export const version = 1190;
|
||||||
|
|
||||||
|
export function updateToSchemaVersion1190(
|
||||||
|
currentVersion: number,
|
||||||
|
db: Database,
|
||||||
|
logger: LoggerType
|
||||||
|
): void {
|
||||||
|
if (currentVersion >= 1190) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
db.exec(`
|
||||||
|
ALTER TABLE callLinks ADD COLUMN storageID TEXT;
|
||||||
|
ALTER TABLE callLinks ADD COLUMN storageVersion INTEGER;
|
||||||
|
ALTER TABLE callLinks ADD COLUMN storageUnknownFields BLOB;
|
||||||
|
ALTER TABLE callLinks ADD COLUMN storageNeedsSync INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE callLinks ADD COLUMN deletedAt INTEGER;
|
||||||
|
`);
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
UPDATE callLinks
|
||||||
|
SET deletedAt = $deletedAt
|
||||||
|
WHERE deleted = 1;
|
||||||
|
`
|
||||||
|
).run({
|
||||||
|
deletedAt: new Date().getTime(),
|
||||||
|
});
|
||||||
|
|
||||||
|
db.pragma('user_version = 1190');
|
||||||
|
})();
|
||||||
|
logger.info('updateToSchemaVersion1190: success!');
|
||||||
|
}
|
|
@ -94,10 +94,11 @@ import { updateToSchemaVersion1140 } from './1140-call-links-deleted-column';
|
||||||
import { updateToSchemaVersion1150 } from './1150-expire-timer-version';
|
import { updateToSchemaVersion1150 } from './1150-expire-timer-version';
|
||||||
import { updateToSchemaVersion1160 } from './1160-optimize-calls-unread-count';
|
import { updateToSchemaVersion1160 } from './1160-optimize-calls-unread-count';
|
||||||
import { updateToSchemaVersion1170 } from './1170-update-call-history-unread-index';
|
import { updateToSchemaVersion1170 } from './1170-update-call-history-unread-index';
|
||||||
|
import { updateToSchemaVersion1180 } from './1180-add-attachment-download-source';
|
||||||
import {
|
import {
|
||||||
updateToSchemaVersion1180,
|
updateToSchemaVersion1190,
|
||||||
version as MAX_VERSION,
|
version as MAX_VERSION,
|
||||||
} from './1180-add-attachment-download-source';
|
} from './1190-call-links-storage';
|
||||||
|
|
||||||
function updateToSchemaVersion1(
|
function updateToSchemaVersion1(
|
||||||
currentVersion: number,
|
currentVersion: number,
|
||||||
|
@ -2060,6 +2061,7 @@ export const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion1160,
|
updateToSchemaVersion1160,
|
||||||
updateToSchemaVersion1170,
|
updateToSchemaVersion1170,
|
||||||
updateToSchemaVersion1180,
|
updateToSchemaVersion1180,
|
||||||
|
updateToSchemaVersion1190,
|
||||||
];
|
];
|
||||||
|
|
||||||
export class DBVersionFromFutureError extends Error {
|
export class DBVersionFromFutureError extends Error {
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||||
import type { CallLinkStateType, CallLinkType } from '../../types/CallLink';
|
import type {
|
||||||
|
CallLinkRecord,
|
||||||
|
CallLinkStateType,
|
||||||
|
CallLinkType,
|
||||||
|
} from '../../types/CallLink';
|
||||||
import {
|
import {
|
||||||
callLinkRestrictionsSchema,
|
callLinkRestrictionsSchema,
|
||||||
callLinkRecordSchema,
|
callLinkRecordSchema,
|
||||||
|
@ -31,6 +35,19 @@ export function getCallLinkByRoomId(
|
||||||
db: ReadableDB,
|
db: ReadableDB,
|
||||||
roomId: string
|
roomId: string
|
||||||
): CallLinkType | undefined {
|
): CallLinkType | undefined {
|
||||||
|
const callLinkRecord = getCallLinkRecordByRoomId(db, roomId);
|
||||||
|
if (!callLinkRecord) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return callLinkFromRecord(callLinkRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When you need to access all the fields (such as deleted and storage fields)
|
||||||
|
export function getCallLinkRecordByRoomId(
|
||||||
|
db: ReadableDB,
|
||||||
|
roomId: string
|
||||||
|
): CallLinkRecord | undefined {
|
||||||
const row = prepare(db, 'SELECT * FROM callLinks WHERE roomId = $roomId').get(
|
const row = prepare(db, 'SELECT * FROM callLinks WHERE roomId = $roomId').get(
|
||||||
{
|
{
|
||||||
roomId,
|
roomId,
|
||||||
|
@ -41,7 +58,7 @@ export function getCallLinkByRoomId(
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return callLinkFromRecord(callLinkRecordSchema.parse(row));
|
return callLinkRecordSchema.parse(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllCallLinks(db: ReadableDB): ReadonlyArray<CallLinkType> {
|
export function getAllCallLinks(db: ReadableDB): ReadonlyArray<CallLinkType> {
|
||||||
|
@ -69,7 +86,11 @@ function _insertCallLink(db: WritableDB, callLink: CallLinkType): void {
|
||||||
name,
|
name,
|
||||||
restrictions,
|
restrictions,
|
||||||
revoked,
|
revoked,
|
||||||
expiration
|
expiration,
|
||||||
|
storageID,
|
||||||
|
storageVersion,
|
||||||
|
storageUnknownFields,
|
||||||
|
storageNeedsSync
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$roomId,
|
$roomId,
|
||||||
$rootKey,
|
$rootKey,
|
||||||
|
@ -77,7 +98,11 @@ function _insertCallLink(db: WritableDB, callLink: CallLinkType): void {
|
||||||
$name,
|
$name,
|
||||||
$restrictions,
|
$restrictions,
|
||||||
$revoked,
|
$revoked,
|
||||||
$expiration
|
$expiration,
|
||||||
|
$storageID,
|
||||||
|
$storageVersion,
|
||||||
|
$storageUnknownFields,
|
||||||
|
$storageNeedsSync
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
).run(data);
|
).run(data);
|
||||||
|
@ -87,6 +112,30 @@ export function insertCallLink(db: WritableDB, callLink: CallLinkType): void {
|
||||||
_insertCallLink(db, callLink);
|
_insertCallLink(db, callLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateCallLink(db: WritableDB, callLink: CallLinkType): void {
|
||||||
|
const { roomId, rootKey } = callLink;
|
||||||
|
assertRoomIdMatchesRootKey(roomId, rootKey);
|
||||||
|
|
||||||
|
const data = callLinkToRecord(callLink);
|
||||||
|
// Do not write roomId or rootKey since they should never change
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
UPDATE callLinks
|
||||||
|
SET
|
||||||
|
adminKey = $adminKey,
|
||||||
|
name = $name,
|
||||||
|
restrictions = $restrictions,
|
||||||
|
revoked = $revoked,
|
||||||
|
expiration = $expiration,
|
||||||
|
storageID = $storageID,
|
||||||
|
storageVersion = $storageVersion,
|
||||||
|
storageUnknownFields = $storageUnknownFields,
|
||||||
|
storageNeedsSync = $storageNeedsSync
|
||||||
|
WHERE roomId = $roomId
|
||||||
|
`
|
||||||
|
).run(data);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateCallLinkState(
|
export function updateCallLinkState(
|
||||||
db: WritableDB,
|
db: WritableDB,
|
||||||
roomId: string,
|
roomId: string,
|
||||||
|
@ -167,7 +216,16 @@ export function deleteCallLinkFromSync(db: WritableDB, roomId: string): void {
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function beginDeleteCallLink(db: WritableDB, roomId: string): void {
|
export type DeleteCallLinkOptions = {
|
||||||
|
storageNeedsSync: boolean;
|
||||||
|
deletedAt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function beginDeleteCallLink(
|
||||||
|
db: WritableDB,
|
||||||
|
roomId: string,
|
||||||
|
options: DeleteCallLinkOptions
|
||||||
|
): void {
|
||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
// If adminKey is null, then we should delete the call link
|
// If adminKey is null, then we should delete the call link
|
||||||
const [deleteNonAdminCallLinksQuery, deleteNonAdminCallLinksParams] = sql`
|
const [deleteNonAdminCallLinksQuery, deleteNonAdminCallLinksParams] = sql`
|
||||||
|
@ -182,11 +240,17 @@ export function beginDeleteCallLink(db: WritableDB, roomId: string): void {
|
||||||
|
|
||||||
// Skip this query if the call is already deleted
|
// Skip this query if the call is already deleted
|
||||||
if (result.changes === 0) {
|
if (result.changes === 0) {
|
||||||
|
const { storageNeedsSync } = options;
|
||||||
|
const deletedAt = options.deletedAt ?? new Date().getTime();
|
||||||
|
|
||||||
// If the admin key is not null, we should mark it for deletion
|
// If the admin key is not null, we should mark it for deletion
|
||||||
const [markAdminCallLinksDeletedQuery, markAdminCallLinksDeletedParams] =
|
const [markAdminCallLinksDeletedQuery, markAdminCallLinksDeletedParams] =
|
||||||
sql`
|
sql`
|
||||||
UPDATE callLinks
|
UPDATE callLinks
|
||||||
SET deleted = 1
|
SET
|
||||||
|
deleted = 1,
|
||||||
|
deletedAt = ${deletedAt},
|
||||||
|
storageNeedsSync = ${storageNeedsSync ? 1 : 0}
|
||||||
WHERE adminKey IS NOT NULL
|
WHERE adminKey IS NOT NULL
|
||||||
AND roomId = ${roomId};
|
AND roomId = ${roomId};
|
||||||
`;
|
`;
|
||||||
|
@ -201,14 +265,21 @@ export function beginDeleteCallLink(db: WritableDB, roomId: string): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function beginDeleteAllCallLinks(db: WritableDB): void {
|
export function beginDeleteAllCallLinks(db: WritableDB): void {
|
||||||
|
const deletedAt = new Date().getTime();
|
||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
const [markAdminCallLinksDeletedQuery] = sql`
|
const [markAdminCallLinksDeletedQuery, markAdminCallLinksDeletedParams] =
|
||||||
|
sql`
|
||||||
UPDATE callLinks
|
UPDATE callLinks
|
||||||
SET deleted = 1
|
SET
|
||||||
|
deleted = 1,
|
||||||
|
deletedAt = ${deletedAt},
|
||||||
|
storageNeedsSync = 1
|
||||||
WHERE adminKey IS NOT NULL;
|
WHERE adminKey IS NOT NULL;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
db.prepare(markAdminCallLinksDeletedQuery).run();
|
db.prepare(markAdminCallLinksDeletedQuery).run(
|
||||||
|
markAdminCallLinksDeletedParams
|
||||||
|
);
|
||||||
|
|
||||||
const [deleteNonAdminCallLinksQuery] = sql`
|
const [deleteNonAdminCallLinksQuery] = sql`
|
||||||
DELETE FROM callLinks
|
DELETE FROM callLinks
|
||||||
|
@ -219,6 +290,21 @@ export function beginDeleteAllCallLinks(db: WritableDB): void {
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When you need to access the deleted field
|
||||||
|
export function getAllCallLinkRecordsWithAdminKey(
|
||||||
|
db: ReadableDB
|
||||||
|
): ReadonlyArray<CallLinkRecord> {
|
||||||
|
const [query] = sql`
|
||||||
|
SELECT * FROM callLinks
|
||||||
|
WHERE adminKey IS NOT NULL
|
||||||
|
AND rootKey IS NOT NULL;
|
||||||
|
`;
|
||||||
|
return db
|
||||||
|
.prepare(query)
|
||||||
|
.all()
|
||||||
|
.map(item => callLinkRecordSchema.parse(item));
|
||||||
|
}
|
||||||
|
|
||||||
export function getAllMarkedDeletedCallLinks(
|
export function getAllMarkedDeletedCallLinks(
|
||||||
db: ReadableDB
|
db: ReadableDB
|
||||||
): ReadonlyArray<CallLinkType> {
|
): ReadonlyArray<CallLinkType> {
|
||||||
|
@ -231,9 +317,13 @@ export function getAllMarkedDeletedCallLinks(
|
||||||
.map(item => callLinkFromRecord(callLinkRecordSchema.parse(item)));
|
.map(item => callLinkFromRecord(callLinkRecordSchema.parse(item)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Run this after uploading storage records, maybe periodically on startup
|
||||||
export function finalizeDeleteCallLink(db: WritableDB, roomId: string): void {
|
export function finalizeDeleteCallLink(db: WritableDB, roomId: string): void {
|
||||||
const [query, params] = sql`
|
const [query, params] = sql`
|
||||||
DELETE FROM callLinks WHERE roomId = ${roomId} AND deleted = 1;
|
DELETE FROM callLinks
|
||||||
|
WHERE roomId = ${roomId}
|
||||||
|
AND deleted = 1
|
||||||
|
AND storageNeedsSync = 0;
|
||||||
`;
|
`;
|
||||||
db.prepare(query).run(params);
|
db.prepare(query).run(params);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
hasScreenCapturePermission,
|
hasScreenCapturePermission,
|
||||||
openSystemPreferences,
|
openSystemPreferences,
|
||||||
} from 'mac-screen-capture-permissions';
|
} from 'mac-screen-capture-permissions';
|
||||||
import { omit, pick } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import type { ReadonlyDeep } from 'type-fest';
|
import type { ReadonlyDeep } from 'type-fest';
|
||||||
import {
|
import {
|
||||||
CallLinkRootKey,
|
CallLinkRootKey,
|
||||||
|
@ -98,6 +98,7 @@ import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||||
import type { StartCallData } from '../../components/ConfirmLeaveCallModal';
|
import type { StartCallData } from '../../components/ConfirmLeaveCallModal';
|
||||||
import { callLinksDeleteJobQueue } from '../../jobs/callLinksDeleteJobQueue';
|
import { callLinksDeleteJobQueue } from '../../jobs/callLinksDeleteJobQueue';
|
||||||
import { getCallLinksByRoomId } from '../selectors/calling';
|
import { getCallLinksByRoomId } from '../selectors/calling';
|
||||||
|
import { storageServiceUploadJob } from '../../services/storage';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -1425,25 +1426,31 @@ function handleCallLinkUpdate(
|
||||||
const logId = `handleCallLinkUpdate(${roomId})`;
|
const logId = `handleCallLinkUpdate(${roomId})`;
|
||||||
|
|
||||||
const freshCallLinkState = await calling.readCallLink(callLinkRootKey);
|
const freshCallLinkState = await calling.readCallLink(callLinkRootKey);
|
||||||
|
const existingCallLink = await DataReader.getCallLinkByRoomId(roomId);
|
||||||
|
|
||||||
// Only give up when server confirms the call link is gone. If we fail to fetch
|
// Only give up when server confirms the call link is gone. If we fail to fetch
|
||||||
// state due to unexpected errors, continue to save rootKey and adminKey.
|
// state due to unexpected errors, continue to save rootKey and adminKey.
|
||||||
if (freshCallLinkState == null) {
|
if (freshCallLinkState == null) {
|
||||||
log.info(`${logId}: Call link not found, ignoring`);
|
log.info(`${logId}: Call link not found on server`);
|
||||||
|
if (!existingCallLink) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingCallLink = await DataReader.getCallLinkByRoomId(roomId);
|
// If the call link is gone remotely (for example if it expired on the server),
|
||||||
const existingCallLinkState = pick(existingCallLink, [
|
// then delete local call link.
|
||||||
'name',
|
log.info(`${logId}: Deleting existing call link`);
|
||||||
'restrictions',
|
await DataWriter.beginDeleteCallLink(roomId, {
|
||||||
'expiration',
|
storageNeedsSync: true,
|
||||||
'revoked',
|
});
|
||||||
]);
|
storageServiceUploadJob();
|
||||||
|
handleCallLinkDelete({ roomId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const callLink: CallLinkType = {
|
const callLink: CallLinkType = {
|
||||||
...CALL_LINK_DEFAULT_STATE,
|
...CALL_LINK_DEFAULT_STATE,
|
||||||
...existingCallLinkState,
|
storageNeedsSync: false,
|
||||||
|
...existingCallLink,
|
||||||
...freshCallLinkState,
|
...freshCallLinkState,
|
||||||
roomId,
|
roomId,
|
||||||
rootKey,
|
rootKey,
|
||||||
|
@ -1482,6 +1489,17 @@ function handleCallLinkUpdate(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCallLinkUpdateLocal(
|
||||||
|
callLink: CallLinkType
|
||||||
|
): ThunkAction<void, RootStateType, unknown, HandleCallLinkUpdateActionType> {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: HANDLE_CALL_LINK_UPDATE,
|
||||||
|
payload: { callLink },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function handleCallLinkDelete(
|
function handleCallLinkDelete(
|
||||||
payload: HandleCallLinkDeleteType
|
payload: HandleCallLinkDeleteType
|
||||||
): ThunkAction<void, RootStateType, unknown, HandleCallLinkDeleteActionType> {
|
): ThunkAction<void, RootStateType, unknown, HandleCallLinkDeleteActionType> {
|
||||||
|
@ -1990,6 +2008,9 @@ function createCallLink(
|
||||||
DataWriter.insertCallLink(callLink),
|
DataWriter.insertCallLink(callLink),
|
||||||
DataWriter.saveCallHistory(callHistory),
|
DataWriter.saveCallHistory(callHistory),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
storageServiceUploadJob();
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: HANDLE_CALL_LINK_UPDATE,
|
type: HANDLE_CALL_LINK_UPDATE,
|
||||||
payload: { callLink },
|
payload: { callLink },
|
||||||
|
@ -2004,7 +2025,8 @@ function deleteCallLink(
|
||||||
roomId: string
|
roomId: string
|
||||||
): ThunkAction<void, RootStateType, unknown, HandleCallLinkDeleteActionType> {
|
): ThunkAction<void, RootStateType, unknown, HandleCallLinkDeleteActionType> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
await DataWriter.beginDeleteCallLink(roomId);
|
await DataWriter.beginDeleteCallLink(roomId, { storageNeedsSync: true });
|
||||||
|
storageServiceUploadJob();
|
||||||
await callLinksDeleteJobQueue.add({ source: 'deleteCallLink' });
|
await callLinksDeleteJobQueue.add({ source: 'deleteCallLink' });
|
||||||
dispatch(handleCallLinkDelete({ roomId }));
|
dispatch(handleCallLinkDelete({ roomId }));
|
||||||
};
|
};
|
||||||
|
@ -2171,6 +2193,7 @@ const _startCallLinkLobby = async ({
|
||||||
restrictions,
|
restrictions,
|
||||||
revoked,
|
revoked,
|
||||||
expiration,
|
expiration,
|
||||||
|
storageNeedsSync: false,
|
||||||
});
|
});
|
||||||
log.info('startCallLinkLobby: Saved new call link', roomId);
|
log.info('startCallLinkLobby: Saved new call link', roomId);
|
||||||
}
|
}
|
||||||
|
@ -2446,6 +2469,7 @@ export const actions = {
|
||||||
groupCallStateChange,
|
groupCallStateChange,
|
||||||
hangUpActiveCall,
|
hangUpActiveCall,
|
||||||
handleCallLinkUpdate,
|
handleCallLinkUpdate,
|
||||||
|
handleCallLinkUpdateLocal,
|
||||||
handleCallLinkDelete,
|
handleCallLinkDelete,
|
||||||
joinedAdhocCall,
|
joinedAdhocCall,
|
||||||
leaveCurrentCallAndStartCallingLobby,
|
leaveCurrentCallAndStartCallingLobby,
|
||||||
|
@ -2670,6 +2694,7 @@ export function reducer(
|
||||||
callLinks[conversationId]?.rootKey ??
|
callLinks[conversationId]?.rootKey ??
|
||||||
action.payload.callLinkRootKey,
|
action.payload.callLinkRootKey,
|
||||||
adminKey: callLinks[conversationId]?.adminKey,
|
adminKey: callLinks[conversationId]?.adminKey,
|
||||||
|
storageNeedsSync: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: callLinks,
|
: callLinks,
|
||||||
|
|
|
@ -13,6 +13,10 @@ export const FAKE_CALL_LINK: CallLinkType = {
|
||||||
revoked: false,
|
revoked: false,
|
||||||
roomId: 'd517b48dd118bee24068d4938886c8abe192706d84936d52594a9157189d2759',
|
roomId: 'd517b48dd118bee24068d4938886c8abe192706d84936d52594a9157189d2759',
|
||||||
rootKey: 'dxbb-xfqz-xkgp-nmrx-bpqn-ptkb-spdt-pdgt',
|
rootKey: 'dxbb-xfqz-xkgp-nmrx-bpqn-ptkb-spdt-pdgt',
|
||||||
|
storageID: undefined,
|
||||||
|
storageVersion: undefined,
|
||||||
|
storageUnknownFields: undefined,
|
||||||
|
storageNeedsSync: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Please set expiration
|
// Please set expiration
|
||||||
|
@ -24,6 +28,10 @@ export const FAKE_CALL_LINK_WITH_ADMIN_KEY: CallLinkType = {
|
||||||
revoked: false,
|
revoked: false,
|
||||||
roomId: 'c097eb04cc278d6bc7ed9fb2ddeac00dc9646ae6ddb38513dad9a8a4fe3c38f4',
|
roomId: 'c097eb04cc278d6bc7ed9fb2ddeac00dc9646ae6ddb38513dad9a8a4fe3c38f4',
|
||||||
rootKey: 'bpmc-mrgn-hntf-mffd-mndd-xbxk-zmgq-qszg',
|
rootKey: 'bpmc-mrgn-hntf-mffd-mndd-xbxk-zmgq-qszg',
|
||||||
|
storageID: undefined,
|
||||||
|
storageVersion: undefined,
|
||||||
|
storageUnknownFields: undefined,
|
||||||
|
storageNeedsSync: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getCallLinkState(callLink: CallLinkType): CallLinkStateType {
|
export function getCallLinkState(callLink: CallLinkType): CallLinkStateType {
|
||||||
|
|
|
@ -73,6 +73,10 @@ describe('backup/calling', () => {
|
||||||
restrictions: CallLinkRestrictions.AdminApproval,
|
restrictions: CallLinkRestrictions.AdminApproval,
|
||||||
revoked: false,
|
revoked: false,
|
||||||
expiration: null,
|
expiration: null,
|
||||||
|
storageID: undefined,
|
||||||
|
storageVersion: undefined,
|
||||||
|
storageUnknownFields: undefined,
|
||||||
|
storageNeedsSync: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
await DataWriter.insertCallLink(callLink);
|
await DataWriter.insertCallLink(callLink);
|
||||||
|
@ -201,6 +205,10 @@ describe('backup/calling', () => {
|
||||||
restrictions: CallLinkRestrictions.AdminApproval,
|
restrictions: CallLinkRestrictions.AdminApproval,
|
||||||
revoked: false,
|
revoked: false,
|
||||||
expiration: null,
|
expiration: null,
|
||||||
|
storageID: undefined,
|
||||||
|
storageVersion: undefined,
|
||||||
|
storageUnknownFields: undefined,
|
||||||
|
storageNeedsSync: false,
|
||||||
};
|
};
|
||||||
await DataWriter.insertCallLink(callLinkNoAdmin);
|
await DataWriter.insertCallLink(callLinkNoAdmin);
|
||||||
|
|
||||||
|
|
|
@ -1368,6 +1368,10 @@ describe('calling duck', () => {
|
||||||
roomId,
|
roomId,
|
||||||
rootKey,
|
rootKey,
|
||||||
adminKey,
|
adminKey,
|
||||||
|
storageID: undefined,
|
||||||
|
storageVersion: undefined,
|
||||||
|
storageUnknownFields: undefined,
|
||||||
|
storageNeedsSync: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1388,6 +1392,10 @@ describe('calling duck', () => {
|
||||||
roomId,
|
roomId,
|
||||||
rootKey,
|
rootKey,
|
||||||
adminKey: 'banana',
|
adminKey: 'banana',
|
||||||
|
storageID: undefined,
|
||||||
|
storageVersion: undefined,
|
||||||
|
storageUnknownFields: undefined,
|
||||||
|
storageNeedsSync: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -115,4 +115,27 @@ describe('Proto#$unknownFields', () => {
|
||||||
assert.strictEqual(decoded.c, 42);
|
assert.strictEqual(decoded.c, 42);
|
||||||
assert.strictEqual(Buffer.from(decoded.d).toString(), 'ohai');
|
assert.strictEqual(Buffer.from(decoded.d).toString(), 'ohai');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not set unknown fields if all fields were known', () => {
|
||||||
|
const encoded = Partial.encode({
|
||||||
|
a: 'hello',
|
||||||
|
c: 42,
|
||||||
|
}).finish();
|
||||||
|
const decoded = Full.decode(encoded);
|
||||||
|
|
||||||
|
assert.strictEqual(decoded.a, 'hello');
|
||||||
|
assert.strictEqual(decoded.c, 42);
|
||||||
|
assert.isUndefined(decoded.$unknownFields);
|
||||||
|
|
||||||
|
const encodedWithEmptyArray = Partial.encode({
|
||||||
|
a: 'hi',
|
||||||
|
c: 69,
|
||||||
|
$unkownFields: [],
|
||||||
|
}).finish();
|
||||||
|
const decodedWithEmptyArray = Full.decode(encodedWithEmptyArray);
|
||||||
|
|
||||||
|
assert.strictEqual(decodedWithEmptyArray.a, 'hi');
|
||||||
|
assert.strictEqual(decodedWithEmptyArray.c, 69);
|
||||||
|
assert.isUndefined(decodedWithEmptyArray.$unknownFields);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3557,10 +3557,6 @@ export default class MessageReceiver
|
||||||
callLinkUpdate.type === Proto.SyncMessage.CallLinkUpdate.Type.UPDATE
|
callLinkUpdate.type === Proto.SyncMessage.CallLinkUpdate.Type.UPDATE
|
||||||
) {
|
) {
|
||||||
callLinkUpdateSyncType = CallLinkUpdateSyncType.Update;
|
callLinkUpdateSyncType = CallLinkUpdateSyncType.Update;
|
||||||
} else if (
|
|
||||||
callLinkUpdate.type === Proto.SyncMessage.CallLinkUpdate.Type.DELETE
|
|
||||||
) {
|
|
||||||
callLinkUpdateSyncType = CallLinkUpdateSyncType.Delete;
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`MessageReceiver.handleCallLinkUpdate: unknown type ${callLinkUpdate.type}`
|
`MessageReceiver.handleCallLinkUpdate: unknown type ${callLinkUpdate.type}`
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { z } from 'zod';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import { safeParseInteger } from '../util/numbers';
|
import { safeParseInteger } from '../util/numbers';
|
||||||
import { byteLength } from '../Bytes';
|
import { byteLength } from '../Bytes';
|
||||||
|
import type { StorageServiceFieldsType } from '../sql/Interface';
|
||||||
|
|
||||||
export enum CallLinkUpdateSyncType {
|
export enum CallLinkUpdateSyncType {
|
||||||
Update = 'Update',
|
Update = 'Update',
|
||||||
|
@ -61,7 +62,8 @@ export type CallLinkType = Readonly<{
|
||||||
// Guaranteed from RingRTC readCallLink, but locally may be null immediately after
|
// Guaranteed from RingRTC readCallLink, but locally may be null immediately after
|
||||||
// CallLinkUpdate sync and before readCallLink
|
// CallLinkUpdate sync and before readCallLink
|
||||||
expiration: number | null;
|
expiration: number | null;
|
||||||
}>;
|
}> &
|
||||||
|
StorageServiceFieldsType;
|
||||||
|
|
||||||
export type CallLinkStateType = Pick<
|
export type CallLinkStateType = Pick<
|
||||||
CallLinkType,
|
CallLinkType,
|
||||||
|
@ -86,6 +88,12 @@ export type CallLinkRecord = Readonly<{
|
||||||
restrictions: number;
|
restrictions: number;
|
||||||
expiration: number | null;
|
expiration: number | null;
|
||||||
revoked: 1 | 0; // sqlite's version of boolean
|
revoked: 1 | 0; // sqlite's version of boolean
|
||||||
|
deleted?: 1 | 0;
|
||||||
|
deletedAt?: number | null;
|
||||||
|
storageID: string | null;
|
||||||
|
storageVersion: number | null;
|
||||||
|
storageUnknownFields: Uint8Array | null;
|
||||||
|
storageNeedsSync: 1 | 0;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export const callLinkRecordSchema = z.object({
|
export const callLinkRecordSchema = z.object({
|
||||||
|
@ -98,6 +106,12 @@ export const callLinkRecordSchema = z.object({
|
||||||
restrictions: callLinkRestrictionsSchema,
|
restrictions: callLinkRestrictionsSchema,
|
||||||
expiration: z.number().int().nullable(),
|
expiration: z.number().int().nullable(),
|
||||||
revoked: z.union([z.literal(1), z.literal(0)]),
|
revoked: z.union([z.literal(1), z.literal(0)]),
|
||||||
|
deleted: z.union([z.literal(1), z.literal(0)]).optional(),
|
||||||
|
deletedAt: z.number().int().nullable().optional(),
|
||||||
|
storageID: z.string().nullable(),
|
||||||
|
storageVersion: z.number().int().nullable(),
|
||||||
|
storageUnknownFields: z.instanceof(Uint8Array).nullable(),
|
||||||
|
storageNeedsSync: z.union([z.literal(1), z.literal(0)]),
|
||||||
}) satisfies z.ZodType<CallLinkRecord>;
|
}) satisfies z.ZodType<CallLinkRecord>;
|
||||||
|
|
||||||
export function isCallLinkAdmin(callLink: CallLinkType): boolean {
|
export function isCallLinkAdmin(callLink: CallLinkType): boolean {
|
||||||
|
|
|
@ -67,6 +67,7 @@ import type { ConversationModel } from '../models/conversations';
|
||||||
import { drop } from './drop';
|
import { drop } from './drop';
|
||||||
import { sendCallLinkUpdateSync } from './sendCallLinkUpdateSync';
|
import { sendCallLinkUpdateSync } from './sendCallLinkUpdateSync';
|
||||||
import { callLinksDeleteJobQueue } from '../jobs/callLinksDeleteJobQueue';
|
import { callLinksDeleteJobQueue } from '../jobs/callLinksDeleteJobQueue';
|
||||||
|
import { storageServiceUploadJob } from '../services/storage';
|
||||||
|
|
||||||
// utils
|
// utils
|
||||||
// -----
|
// -----
|
||||||
|
@ -1303,6 +1304,7 @@ export async function clearCallHistoryDataAndSync(
|
||||||
);
|
);
|
||||||
const messageIds = await DataWriter.clearCallHistory(latestCall);
|
const messageIds = await DataWriter.clearCallHistory(latestCall);
|
||||||
await DataWriter.beginDeleteAllCallLinks();
|
await DataWriter.beginDeleteAllCallLinks();
|
||||||
|
storageServiceUploadJob();
|
||||||
updateDeletedMessages(messageIds);
|
updateDeletedMessages(messageIds);
|
||||||
log.info('clearCallHistory: Queueing sync message');
|
log.info('clearCallHistory: Queueing sync message');
|
||||||
await singleProtoJobQueue.add(
|
await singleProtoJobQueue.add(
|
||||||
|
|
|
@ -15,14 +15,18 @@ import {
|
||||||
type CallHistoryDetails,
|
type CallHistoryDetails,
|
||||||
CallMode,
|
CallMode,
|
||||||
} from '../types/CallDisposition';
|
} from '../types/CallDisposition';
|
||||||
|
import { DAY } from './durations';
|
||||||
|
|
||||||
export const CALL_LINK_DEFAULT_STATE = {
|
export const CALL_LINK_DEFAULT_STATE: Partial<CallLinkType> = {
|
||||||
name: '',
|
name: '',
|
||||||
restrictions: CallLinkRestrictions.Unknown,
|
restrictions: CallLinkRestrictions.Unknown,
|
||||||
revoked: false,
|
revoked: false,
|
||||||
expiration: null,
|
expiration: null,
|
||||||
|
storageNeedsSync: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CALL_LINK_DELETED_STORAGE_RECORD_TTL = 30 * DAY;
|
||||||
|
|
||||||
export function getKeyFromCallLink(callLink: string): string {
|
export function getKeyFromCallLink(callLink: string): string {
|
||||||
const url = new URL(callLink);
|
const url = new URL(callLink);
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
|
|
|
@ -136,6 +136,10 @@ export function callLinkFromRecord(record: CallLinkRecord): CallLinkType {
|
||||||
restrictions: toCallLinkRestrictions(record.restrictions),
|
restrictions: toCallLinkRestrictions(record.restrictions),
|
||||||
revoked: record.revoked === 1,
|
revoked: record.revoked === 1,
|
||||||
expiration: record.expiration,
|
expiration: record.expiration,
|
||||||
|
storageID: record.storageID || undefined,
|
||||||
|
storageVersion: record.storageVersion || undefined,
|
||||||
|
storageUnknownFields: record.storageUnknownFields || undefined,
|
||||||
|
storageNeedsSync: record.storageNeedsSync === 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,5 +160,9 @@ export function callLinkToRecord(callLink: CallLinkType): CallLinkRecord {
|
||||||
restrictions: callLink.restrictions,
|
restrictions: callLink.restrictions,
|
||||||
revoked: callLink.revoked ? 1 : 0,
|
revoked: callLink.revoked ? 1 : 0,
|
||||||
expiration: callLink.expiration,
|
expiration: callLink.expiration,
|
||||||
|
storageID: callLink.storageID || null,
|
||||||
|
storageVersion: callLink.storageVersion || null,
|
||||||
|
storageUnknownFields: callLink.storageUnknownFields || null,
|
||||||
|
storageNeedsSync: callLink.storageNeedsSync ? 1 : 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,15 +21,6 @@ export async function sendCallLinkUpdateSync(
|
||||||
return _sendCallLinkUpdateSync(callLink, CallLinkUpdateSyncType.Update);
|
return _sendCallLinkUpdateSync(callLink, CallLinkUpdateSyncType.Update);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Underlying sync message is CallLinkUpdate with type set to DELETE.
|
|
||||||
*/
|
|
||||||
export async function sendCallLinkDeleteSync(
|
|
||||||
callLink: sendCallLinkUpdateSyncCallLinkType
|
|
||||||
): Promise<void> {
|
|
||||||
return _sendCallLinkUpdateSync(callLink, CallLinkUpdateSyncType.Delete);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _sendCallLinkUpdateSync(
|
async function _sendCallLinkUpdateSync(
|
||||||
callLink: sendCallLinkUpdateSyncCallLinkType,
|
callLink: sendCallLinkUpdateSyncCallLinkType,
|
||||||
type: CallLinkUpdateSyncType
|
type: CallLinkUpdateSyncType
|
||||||
|
@ -37,8 +28,6 @@ async function _sendCallLinkUpdateSync(
|
||||||
let protoType: Proto.SyncMessage.CallLinkUpdate.Type;
|
let protoType: Proto.SyncMessage.CallLinkUpdate.Type;
|
||||||
if (type === CallLinkUpdateSyncType.Update) {
|
if (type === CallLinkUpdateSyncType.Update) {
|
||||||
protoType = Proto.SyncMessage.CallLinkUpdate.Type.UPDATE;
|
protoType = Proto.SyncMessage.CallLinkUpdate.Type.UPDATE;
|
||||||
} else if (type === CallLinkUpdateSyncType.Delete) {
|
|
||||||
protoType = Proto.SyncMessage.CallLinkUpdate.Type.DELETE;
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`sendCallLinkUpdateSync: unknown type ${type}`);
|
throw new Error(`sendCallLinkUpdateSync: unknown type ${type}`);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue