Support delete for call links

Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
Jamie Kyle 2024-08-06 12:29:13 -07:00 committed by GitHub
parent 11fed7e7f8
commit 9a9f9495f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 853 additions and 345 deletions

View file

@ -562,6 +562,7 @@ type ReadableInterface = {
callLinkExists(roomId: string): boolean;
getAllCallLinks: () => ReadonlyArray<CallLinkType>;
getCallLinkByRoomId: (roomId: string) => CallLinkType | undefined;
getAllMarkedDeletedCallLinks(): ReadonlyArray<CallLinkType>;
getMessagesBetween: (
conversationId: string,
options: GetMessagesBetweenOptions
@ -794,6 +795,10 @@ type WritableInterface = {
roomId: string,
callLinkState: CallLinkStateType
): CallLinkType;
beginDeleteAllCallLinks(): void;
beginDeleteCallLink(roomId: string): void;
finalizeDeleteCallLink(roomId: string): void;
deleteCallLinkFromSync(roomId: string): void;
migrateConversationMessages: (obsoleteId: string, currentId: string) => void;
saveEditedMessage: (
mainMessage: ReadonlyDeep<MessageType>,

View file

@ -168,6 +168,7 @@ import {
GroupCallStatus,
CallType,
CallStatusValue,
CallMode,
} from '../types/CallDisposition';
import {
callLinkExists,
@ -176,13 +177,17 @@ import {
insertCallLink,
updateCallLinkAdminKeyByRoomId,
updateCallLinkState,
beginDeleteAllCallLinks,
getAllMarkedDeletedCallLinks,
finalizeDeleteCallLink,
beginDeleteCallLink,
deleteCallLinkFromSync,
} from './server/callLinks';
import {
replaceAllEndorsementsForGroup,
deleteAllEndorsementsForGroup,
getGroupSendCombinedEndorsementExpiration,
} from './server/groupEndorsements';
import { CallMode } from '../types/Calling';
import {
attachmentDownloadJobSchema,
type AttachmentDownloadJobType,
@ -296,6 +301,7 @@ export const DataReader: ServerReadableInterface = {
callLinkExists,
getAllCallLinks,
getCallLinkByRoomId,
getAllMarkedDeletedCallLinks,
getMessagesBetween,
getNearbyMessageFromDeletedSet,
getMostRecentAddressableMessages,
@ -430,6 +436,10 @@ export const DataWriter: ServerWritableInterface = {
insertCallLink,
updateCallLinkAdminKeyByRoomId,
updateCallLinkState,
beginDeleteAllCallLinks,
beginDeleteCallLink,
finalizeDeleteCallLink,
deleteCallLinkFromSync,
migrateConversationMessages,
saveEditedMessage,
saveEditedMessages,
@ -3458,7 +3468,7 @@ function clearCallHistory(
return db.transaction(() => {
const timestamp = getMessageTimestampForCallLogEventTarget(db, target);
const [selectCallIdsQuery, selectCallIdsParams] = sql`
const [selectCallsQuery, selectCallsParams] = sql`
SELECT callsHistory.callId
FROM callsHistory
WHERE
@ -3471,18 +3481,30 @@ function clearCallHistory(
);
`;
const callIds = db
.prepare(selectCallIdsQuery)
const deletedCallIds: ReadonlyArray<string> = db
.prepare(selectCallsQuery)
.pluck()
.all(selectCallIdsParams);
.all(selectCallsParams);
let deletedMessageIds: ReadonlyArray<string> = [];
batchMultiVarQuery(db, callIds, (ids: ReadonlyArray<string>): void => {
batchMultiVarQuery(db, deletedCallIds, (ids): void => {
const idsFragment = sqlJoin(ids);
const [clearCallsHistoryQuery, clearCallsHistoryParams] = sql`
UPDATE callsHistory
SET
status = ${DirectCallStatus.Deleted},
timestamp = ${Date.now()}
WHERE callsHistory.callId IN (${idsFragment});
`;
db.prepare(clearCallsHistoryQuery).run(clearCallsHistoryParams);
const [deleteMessagesQuery, deleteMessagesParams] = sql`
DELETE FROM messages
WHERE messages.type IS 'call-history'
AND messages.callId IN (${sqlJoin(ids)})
AND messages.callId IN (${idsFragment})
RETURNING id;
`;
@ -3494,21 +3516,6 @@ function clearCallHistory(
deletedMessageIds = deletedMessageIds.concat(batchDeletedMessageIds);
});
const [clearCallsHistoryQuery, clearCallsHistoryParams] = sql`
UPDATE callsHistory
SET
status = ${DirectCallStatus.Deleted},
timestamp = ${Date.now()}
WHERE callsHistory.timestamp <= ${timestamp};
`;
try {
db.prepare(clearCallsHistoryQuery).run(clearCallsHistoryParams);
} catch (error) {
logger.error(error, error.message);
throw error;
}
return deletedMessageIds;
})();
}
@ -3519,9 +3526,8 @@ function markCallHistoryDeleted(db: WritableDB, callId: string): void {
SET
status = ${DirectCallStatus.Deleted},
timestamp = ${Date.now()}
WHERE callId = ${callId};
WHERE callId = ${callId}
`;
db.prepare(query).run(params);
}
@ -4829,7 +4835,7 @@ function getNextAttachmentBackupJobs(
active = 0
AND
(retryAfter is NULL OR retryAfter <= ${timestamp})
ORDER BY
ORDER BY
-- type is "standard" or "thumbnail"; we prefer "standard" jobs
type ASC, receivedAt DESC
LIMIT ${limit}

View file

@ -0,0 +1,31 @@
// 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 = 1140;
export function updateToSchemaVersion1140(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 1140) {
return;
}
db.transaction(() => {
db.exec(`
DROP INDEX IF EXISTS callLinks_deleted;
ALTER TABLE callLinks
ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;
CREATE INDEX callLinks_deleted
ON callLinks (deleted, roomId);
`);
})();
db.pragma('user_version = 1140');
logger.info('updateToSchemaVersion1140: success!');
}

View file

@ -17,8 +17,8 @@ import {
CallType,
GroupCallStatus,
callHistoryDetailsSchema,
CallMode,
} from '../../types/CallDisposition';
import { CallMode } from '../../types/Calling';
import type { WritableDB, MessageType, ConversationType } from '../Interface';
import { strictAssert } from '../../util/assert';
import { missingCaseError } from '../../util/missingCaseError';

View file

@ -89,10 +89,11 @@ import { updateToSchemaVersion1090 } from './1090-message-delete-indexes';
import { updateToSchemaVersion1100 } from './1100-optimize-mark-call-history-read-in-conversation';
import { updateToSchemaVersion1110 } from './1110-sticker-local-key';
import { updateToSchemaVersion1120 } from './1120-messages-foreign-keys-indexes';
import { updateToSchemaVersion1130 } from './1130-isStory-index';
import {
updateToSchemaVersion1130,
updateToSchemaVersion1140,
version as MAX_VERSION,
} from './1130-isStory-index';
} from './1140-call-links-deleted-column';
function updateToSchemaVersion1(
currentVersion: number,
@ -2050,6 +2051,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion1110,
updateToSchemaVersion1120,
updateToSchemaVersion1130,
updateToSchemaVersion1140,
];
export class DBVersionFromFutureError extends Error {

View file

@ -7,15 +7,16 @@ import {
callLinkRestrictionsSchema,
callLinkRecordSchema,
} from '../../types/CallLink';
import { toAdminKeyBytes } from '../../util/callLinks';
import {
callLinkToRecord,
callLinkFromRecord,
toAdminKeyBytes,
} from '../../util/callLinks';
} from '../../util/callLinksRingrtc';
import type { ReadableDB, WritableDB } from '../Interface';
import { prepare } from '../Server';
import { sql } from '../util';
import { strictAssert } from '../../util/assert';
import { CallStatusValue } from '../../types/CallDisposition';
export function callLinkExists(db: ReadableDB, roomId: string): boolean {
const [query, params] = sql`
@ -133,3 +134,106 @@ function assertRoomIdMatchesRootKey(roomId: string, rootKey: string): void {
'passed roomId must match roomId derived from root key'
);
}
function deleteCallHistoryByRoomId(db: WritableDB, roomId: string) {
const [
markCallHistoryDeleteByPeerIdQuery,
markCallHistoryDeleteByPeerIdParams,
] = sql`
UPDATE callsHistory
SET
status = ${CallStatusValue.Deleted},
timestamp = ${Date.now()}
WHERE peerId = ${roomId}
`;
db.prepare(markCallHistoryDeleteByPeerIdQuery).run(
markCallHistoryDeleteByPeerIdParams
);
}
// This should only be called from a sync message to avoid accidentally deleting
// on the client but not the server
export function deleteCallLinkFromSync(db: WritableDB, roomId: string): void {
db.transaction(() => {
const [query, params] = sql`
DELETE FROM callLinks
WHERE roomId = ${roomId};
`;
db.prepare(query).run(params);
deleteCallHistoryByRoomId(db, roomId);
})();
}
export function beginDeleteCallLink(db: WritableDB, roomId: string): void {
db.transaction(() => {
// If adminKey is null, then we should delete the call link
const [deleteNonAdminCallLinksQuery, deleteNonAdminCallLinksParams] = sql`
DELETE FROM callLinks
WHERE adminKey IS NULL
AND roomId = ${roomId};
`;
const result = db
.prepare(deleteNonAdminCallLinksQuery)
.run(deleteNonAdminCallLinksParams);
// Skip this query if the call is already deleted
if (result.changes === 0) {
// If the admin key is not null, we should mark it for deletion
const [markAdminCallLinksDeletedQuery, markAdminCallLinksDeletedParams] =
sql`
UPDATE callLinks
SET deleted = 1
WHERE adminKey IS NOT NULL
AND roomId = ${roomId};
`;
db.prepare(markAdminCallLinksDeletedQuery).run(
markAdminCallLinksDeletedParams
);
}
deleteCallHistoryByRoomId(db, roomId);
})();
}
export function beginDeleteAllCallLinks(db: WritableDB): void {
db.transaction(() => {
const [markAdminCallLinksDeletedQuery] = sql`
UPDATE callLinks
SET deleted = 1
WHERE adminKey IS NOT NULL;
`;
db.prepare(markAdminCallLinksDeletedQuery).run();
const [deleteNonAdminCallLinksQuery] = sql`
DELETE FROM callLinks
WHERE adminKey IS NULL;
`;
db.prepare(deleteNonAdminCallLinksQuery).run();
})();
}
export function getAllMarkedDeletedCallLinks(
db: ReadableDB
): ReadonlyArray<CallLinkType> {
const [query] = sql`
SELECT * FROM callLinks WHERE deleted = 1;
`;
return db
.prepare(query)
.all()
.map(item => callLinkFromRecord(callLinkRecordSchema.parse(item)));
}
export function finalizeDeleteCallLink(db: WritableDB, roomId: string): void {
const [query, params] = sql`
DELETE FROM callLinks WHERE roomId = ${roomId} AND deleted = 1;
`;
db.prepare(query).run(params);
}