215 lines
6.2 KiB
TypeScript
215 lines
6.2 KiB
TypeScript
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import type { Database } from '@signalapp/better-sqlite3';
|
|
|
|
import { callIdFromEra } from '@signalapp/ringrtc';
|
|
import Long from 'long';
|
|
|
|
import type { LoggerType } from '../../types/Logging';
|
|
import { sql } from '../util';
|
|
import { getOurUuid } from './41-uuid-keys';
|
|
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
|
import {
|
|
DirectCallStatus,
|
|
CallDirection,
|
|
CallType,
|
|
GroupCallStatus,
|
|
callHistoryDetailsSchema,
|
|
} from '../../types/CallDisposition';
|
|
import { CallMode } from '../../types/Calling';
|
|
|
|
export default function updateToSchemaVersion87(
|
|
currentVersion: number,
|
|
db: Database,
|
|
logger: LoggerType
|
|
): void {
|
|
if (currentVersion >= 87) {
|
|
return;
|
|
}
|
|
|
|
db.transaction(() => {
|
|
const ourUuid = getOurUuid(db);
|
|
|
|
const [createTable] = sql`
|
|
DROP TABLE IF EXISTS callsHistory;
|
|
|
|
CREATE TABLE callsHistory (
|
|
callId TEXT PRIMARY KEY,
|
|
peerId TEXT NOT NULL, -- conversation id (legacy) | uuid | groupId | roomId
|
|
ringerId TEXT DEFAULT NULL, -- ringer uuid
|
|
mode TEXT NOT NULL, -- enum "Direct" | "Group"
|
|
type TEXT NOT NULL, -- enum "Audio" | "Video" | "Group"
|
|
direction TEXT NOT NULL, -- enum "Incoming" | "Outgoing
|
|
-- Direct: enum "Pending" | "Missed" | "Accepted" | "Deleted"
|
|
-- Group: enum "GenericGroupCall" | "OutgoingRing" | "Ringing" | "Joined" | "Missed" | "Declined" | "Accepted" | "Deleted"
|
|
status TEXT NOT NULL,
|
|
timestamp INTEGER NOT NULL,
|
|
UNIQUE (callId, peerId) ON CONFLICT FAIL
|
|
);
|
|
|
|
CREATE INDEX callsHistory_order on callsHistory (timestamp DESC);
|
|
CREATE INDEX callsHistory_byConversation ON callsHistory (peerId);
|
|
-- For 'getCallHistoryGroupData':
|
|
-- This index should target the subqueries for 'possible_parent' and 'possible_children'
|
|
CREATE INDEX callsHistory_callAndGroupInfo_optimize on callsHistory (
|
|
direction,
|
|
peerId,
|
|
timestamp DESC,
|
|
status
|
|
);
|
|
`;
|
|
|
|
db.exec(createTable);
|
|
|
|
const [selectQuery] = sql`
|
|
SELECT * FROM messages
|
|
WHERE type = 'call-history'
|
|
AND callId IS NOT NULL; -- Older messages don't have callId
|
|
`;
|
|
|
|
const rows = db.prepare(selectQuery).all();
|
|
|
|
const uniqueConstraint = new Set();
|
|
|
|
for (const row of rows) {
|
|
const json = JSON.parse(row.json);
|
|
const details = json.callHistoryDetails;
|
|
|
|
const { conversationId: peerId } = row;
|
|
const { callMode } = details;
|
|
|
|
let callId: string;
|
|
let type: CallType;
|
|
let direction: CallDirection;
|
|
let status: GroupCallStatus | DirectCallStatus;
|
|
let timestamp: number;
|
|
let ringerId: string | null = null;
|
|
|
|
if (details.callMode === CallMode.Direct) {
|
|
callId = details.callId;
|
|
type = details.wasVideoCall ? CallType.Video : CallType.Audio;
|
|
direction = details.wasIncoming
|
|
? CallDirection.Incoming
|
|
: CallDirection.Outgoing;
|
|
if (details.acceptedTime != null) {
|
|
status = DirectCallStatus.Accepted;
|
|
} else {
|
|
status = details.wasDeclined
|
|
? DirectCallStatus.Declined
|
|
: DirectCallStatus.Missed;
|
|
}
|
|
timestamp = details.endedTime ?? details.acceptedTime ?? null;
|
|
} else if (details.callMode === CallMode.Group) {
|
|
callId = Long.fromValue(callIdFromEra(details.eraId)).toString();
|
|
type = CallType.Group;
|
|
direction =
|
|
details.creatorUuid === ourUuid
|
|
? CallDirection.Outgoing
|
|
: CallDirection.Incoming;
|
|
status = GroupCallStatus.GenericGroupCall;
|
|
timestamp = details.startedTime;
|
|
ringerId = details.creatorUuid;
|
|
} else {
|
|
logger.error(
|
|
`updateToSchemaVersion87: unknown callMode: ${details.callMode}`
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (callId == null) {
|
|
logger.error(
|
|
"updateToSchemaVersion87: callId doesn't exist, too old, skipping"
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const callHistory: CallHistoryDetails = {
|
|
callId,
|
|
peerId,
|
|
ringerId,
|
|
mode: callMode,
|
|
type,
|
|
direction,
|
|
status,
|
|
timestamp,
|
|
};
|
|
|
|
const result = callHistoryDetailsSchema.safeParse(callHistory);
|
|
|
|
if (!result.success) {
|
|
logger.error(
|
|
`updateToSchemaVersion87: invalid callHistoryDetails (error: ${JSON.stringify(
|
|
result.error.format()
|
|
)}, input: ${JSON.stringify(json)}, output: ${JSON.stringify(
|
|
callHistory
|
|
)}))`
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// We need to ensure a call with the same callId and peerId doesn't get
|
|
// inserted twice because of the unique constraint on the table.
|
|
const uniqueKey = `${callId} -> ${peerId}`;
|
|
if (uniqueConstraint.has(uniqueKey)) {
|
|
logger.error(
|
|
`updateToSchemaVersion87: duplicate callId/peerId pair (${uniqueKey})`
|
|
);
|
|
continue;
|
|
}
|
|
uniqueConstraint.add(uniqueKey);
|
|
|
|
const [insertQuery, insertParams] = sql`
|
|
INSERT INTO callsHistory (
|
|
callId,
|
|
peerId,
|
|
ringerId,
|
|
mode,
|
|
type,
|
|
direction,
|
|
status,
|
|
timestamp
|
|
) VALUES (
|
|
${callHistory.callId},
|
|
${callHistory.peerId},
|
|
${callHistory.ringerId},
|
|
${callHistory.mode},
|
|
${callHistory.type},
|
|
${callHistory.direction},
|
|
${callHistory.status},
|
|
${callHistory.timestamp}
|
|
)
|
|
`;
|
|
|
|
db.prepare(insertQuery).run(insertParams);
|
|
|
|
const [updateQuery, updateParams] = sql`
|
|
UPDATE messages
|
|
SET json = JSON_PATCH(json, ${JSON.stringify({
|
|
callHistoryDetails: null, // delete
|
|
callId,
|
|
})})
|
|
WHERE id = ${row.id}
|
|
`;
|
|
|
|
db.prepare(updateQuery).run(updateParams);
|
|
}
|
|
|
|
const [updateMessagesTable] = sql`
|
|
DROP INDEX IF EXISTS messages_call;
|
|
|
|
ALTER TABLE messages
|
|
DROP COLUMN callId;
|
|
ALTER TABLE messages
|
|
ADD COLUMN callId TEXT;
|
|
ALTER TABLE messages
|
|
DROP COLUMN callMode;
|
|
`;
|
|
|
|
db.exec(updateMessagesTable);
|
|
|
|
db.pragma('user_version = 87');
|
|
})();
|
|
|
|
logger.info('updateToSchemaVersion87: success!');
|
|
}
|