Fix backup import for unused admin call links

This commit is contained in:
ayumi-signal 2025-01-09 12:44:52 -08:00 committed by GitHub
parent 1e7d259909
commit 79d3a0f1ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 116 additions and 44 deletions

View file

@ -134,7 +134,10 @@ import { getBackupCdnInfo } from './util/mediaId';
import { calculateExpirationTimestamp } from '../../util/expirationTimer';
import { ReadStatus } from '../../messages/MessageReadStatus';
import { CallLinkRestrictions } from '../../types/CallLink';
import { toAdminKeyBytes } from '../../util/callLinks';
import {
isCallHistoryForUnusedCallLink,
toAdminKeyBytes,
} from '../../util/callLinks';
import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc';
import { SeenStatus } from '../../MessageSeenStatus';
import { migrateAllMessages } from '../../messages/migrateMessageData';
@ -512,7 +515,7 @@ export class BackupExportStream extends Readable {
for (const item of allCallHistoryItems) {
const { callId, type, peerId: roomId, status, timestamp } = item;
if (type !== CallType.Adhoc) {
if (type !== CallType.Adhoc || isCallHistoryForUnusedCallLink(item)) {
continue;
}

View file

@ -58,6 +58,7 @@ import { assertDev, strictAssert } from '../../util/assert';
import {
getCheckedTimestampFromLong,
getCheckedTimestampOrUndefinedFromLong,
getTimestampOrUndefinedFromLong,
} from '../../util/timestampLongUtils';
import { MAX_SAFE_DATE } from '../../util/timestamp';
import { DurationInSeconds, SECOND } from '../../util/durations';
@ -108,10 +109,13 @@ import {
GroupCallStatus,
} from '../../types/CallDisposition';
import type { CallHistoryDetails } from '../../types/CallDisposition';
import { CallLinkRestrictions } from '../../types/CallLink';
import { CallLinkRestrictions, isCallLinkAdmin } from '../../types/CallLink';
import type { CallLinkType } from '../../types/CallLink';
import type { RawBodyRange } from '../../types/BodyRange';
import { fromAdminKeyBytes } from '../../util/callLinks';
import {
fromAdminKeyBytes,
toCallHistoryFromUnusedCallLink,
} from '../../util/callLinks';
import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc';
import { loadAllAndReinitializeRedux } from '../allLoaders';
import {
@ -209,6 +213,7 @@ export class BackupImportStream extends Writable {
ConversationAttributesType
>();
private readonly recipientIdToCallLink = new Map<number, CallLinkType>();
private readonly adminCallLinksToHasCall = new Map<CallLinkType, boolean>();
private readonly chatIdToConvo = new Map<
number,
ConversationAttributesType
@ -326,6 +331,15 @@ export class BackupImportStream extends Writable {
// Store sticker packs and schedule downloads
await createPacksFromBackup(this.stickerPacks);
// Add placeholder call history for unused admin call links to show in calls tab
for (const [callLink, hasCall] of this.adminCallLinksToHasCall) {
if (!hasCall) {
const callHistory = toCallHistoryFromUnusedCallLink(callLink);
// eslint-disable-next-line no-await-in-loop
await this.saveCallHistory(callHistory);
}
}
// Reset and reload conversations and storage again
window.ConversationController.reset();
@ -1223,12 +1237,19 @@ export class BackupImportStream extends Writable {
name,
restrictions: fromCallLinkRestrictionsProto(restrictions),
revoked: false,
expiration: getCheckedTimestampOrUndefinedFromLong(expirationMs) ?? null,
expiration: getTimestampOrUndefinedFromLong(expirationMs) ?? null,
storageNeedsSync: false,
};
this.recipientIdToCallLink.set(recipientId, callLink);
if (
isCallLinkAdmin(callLink) &&
!this.adminCallLinksToHasCall.has(callLink)
) {
this.adminCallLinksToHasCall.set(callLink, false);
}
await DataWriter.insertCallLink(callLink);
}
@ -3172,6 +3193,10 @@ export class BackupImportStream extends Writable {
};
await this.saveCallHistory(callHistory);
if (isCallLinkAdmin(callLink)) {
this.adminCallLinksToHasCall.set(callLink, true);
}
}
private async fromCustomChatColors(

View file

@ -39,7 +39,6 @@ const GROUP_ID_STRING = Bytes.toBase64(deriveGroupID(GROUP_SECRET_PARAMS));
describe('backup/calling', () => {
let contactA: ConversationModel;
let groupA: ConversationModel;
let callLink: CallLinkType;
beforeEach(async () => {
await DataWriter.removeAll();
@ -64,24 +63,6 @@ describe('backup/calling', () => {
}
);
const rootKey = CallLinkRootKey.generate();
const adminKey = CallLinkRootKey.generateAdminPassKey();
callLink = {
rootKey: rootKey.toString(),
roomId: getRoomIdFromRootKey(rootKey),
adminKey: fromAdminKeyBytes(adminKey),
name: "Let's Talk Rocks",
restrictions: CallLinkRestrictions.AdminApproval,
revoked: false,
expiration: null,
storageID: undefined,
storageVersion: undefined,
storageUnknownFields: undefined,
storageNeedsSync: false,
};
await DataWriter.insertCallLink(callLink);
await loadAllAndReinitializeRedux();
});
after(async () => {
@ -137,6 +118,7 @@ describe('backup/calling', () => {
assert.deepEqual(callHistory, allCallHistory[0]);
});
});
describe('Group calls', () => {
it('roundtrips with a missed call', async () => {
const now = Date.now();
@ -187,22 +169,28 @@ describe('backup/calling', () => {
});
});
describe('Call Links', () => {
it('roundtrips with a link with admin details', async () => {
const allCallLinksBefore = await DataReader.getAllCallLinks();
assert.strictEqual(allCallLinksBefore.length, 1);
let callLink: CallLinkType;
let adminCallLink: CallLinkType;
await symmetricRoundtripHarness([]);
const allCallLinks = await DataReader.getAllCallLinks();
assert.strictEqual(allCallLinks.length, 1);
assert.deepEqual(callLink, allCallLinks[0]);
});
it('roundtrips with a link without admin details', async () => {
await DataWriter._removeAllCallLinks();
beforeEach(async () => {
const adminRootKey = CallLinkRootKey.generate();
const adminKey = CallLinkRootKey.generateAdminPassKey();
adminCallLink = {
rootKey: adminRootKey.toString(),
roomId: getRoomIdFromRootKey(adminRootKey),
adminKey: fromAdminKeyBytes(adminKey),
name: "Let's Talk Rocks",
restrictions: CallLinkRestrictions.AdminApproval,
revoked: false,
expiration: null,
storageID: undefined,
storageVersion: undefined,
storageUnknownFields: undefined,
storageNeedsSync: false,
};
const rootKey = CallLinkRootKey.generate();
const callLinkNoAdmin = {
callLink = {
rootKey: rootKey.toString(),
roomId: getRoomIdFromRootKey(rootKey),
adminKey: null,
@ -215,7 +203,15 @@ describe('backup/calling', () => {
storageUnknownFields: undefined,
storageNeedsSync: false,
};
await DataWriter.insertCallLink(callLinkNoAdmin);
await DataWriter.insertCallLink(callLink);
await loadAllAndReinitializeRedux();
});
it('roundtrips with a link with admin details', async () => {
await DataWriter._removeAllCallLinks();
await DataWriter.insertCallLink(adminCallLink);
const allCallLinksBefore = await DataReader.getAllCallLinks();
assert.strictEqual(allCallLinksBefore.length, 1);
@ -225,11 +221,37 @@ describe('backup/calling', () => {
const allCallLinks = await DataReader.getAllCallLinks();
assert.strictEqual(allCallLinks.length, 1);
assert.deepEqual(callLinkNoAdmin, allCallLinks[0]);
assert.deepEqual(adminCallLink, allCallLinks[0]);
});
});
describe('Adhoc calls', () => {
it('roundtrips with a joined call', async () => {
it('creates placeholder call history for a link with admin details', async () => {
await DataWriter._removeAllCallLinks();
await DataWriter.insertCallLink(adminCallLink);
const allCallHistoryBefore = await DataReader.getAllCallHistory();
assert.strictEqual(allCallHistoryBefore.length, 0);
await symmetricRoundtripHarness([]);
const allCallHistory = await DataReader.getAllCallHistory();
assert.strictEqual(allCallHistory.length, 1);
});
it('roundtrips with a link without admin details', async () => {
await DataWriter._removeAllCallLinks();
await DataWriter.insertCallLink(callLink);
const allCallLinksBefore = await DataReader.getAllCallLinks();
assert.strictEqual(allCallLinksBefore.length, 1);
await symmetricRoundtripHarness([]);
const allCallLinks = await DataReader.getAllCallLinks();
assert.strictEqual(allCallLinks.length, 1);
assert.deepEqual(callLink, allCallLinks[0]);
});
it('roundtrips with a joined adhoc call', async () => {
const now = Date.now();
const callId = '333333';
const callHistory: CallHistoryDetails = {
@ -254,8 +276,7 @@ describe('backup/calling', () => {
assert.deepEqual(callHistory, allCallHistory[0]);
});
it('does not roundtrip call with missing call link', async () => {
it('does not roundtrip adhoc call with missing call link', async () => {
const now = Date.now();
const callId = '44444';
const callHistory: CallHistoryDetails = {

View file

@ -116,3 +116,26 @@ export function toCallHistoryFromUnusedCallLink(
status: AdhocCallStatus.Pending,
};
}
export function isCallHistoryForUnusedCallLink(
callHistory: CallHistoryDetails
): boolean {
const {
ringerId,
startedById,
endedTimestamp,
mode,
type,
direction,
status,
} = callHistory;
return (
ringerId == null &&
startedById == null &&
endedTimestamp == null &&
mode === CallMode.Adhoc &&
type === CallType.Adhoc &&
direction === CallDirection.Incoming &&
status === AdhocCallStatus.Pending
);
}