Cleanup stale ringing calls

This commit is contained in:
Jamie Kyle 2024-02-08 10:01:30 -08:00 committed by GitHub
parent a751eab67d
commit e69826dcc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 200 additions and 2 deletions

View file

@ -351,7 +351,8 @@ export class CallingClass {
reduxInterface.setPresenting();
});
void this.cleanExpiredGroupCallRingsAndLoop();
drop(this.cleanExpiredGroupCallRingsAndLoop());
drop(this.cleanupStaleRingingCalls());
if (process.platform === 'darwin') {
drop(this.enumerateMediaDevices());
@ -593,6 +594,27 @@ export class CallingClass {
);
}
public async cleanupStaleRingingCalls(): Promise<void> {
const calls = await dataInterface.getRecentStaleRingsAndMarkOlderMissed();
const results = await Promise.all(
calls.map(async call => {
const peekInfo = await this.peekGroupCall(call.peerId);
return { callId: call.callId, peekInfo };
})
);
const staleCallIds = results
.filter(result => {
return result.peekInfo == null;
})
.map(result => {
return result.callId;
});
await dataInterface.markCallHistoryMissed(staleCallIds);
}
public async peekGroupCall(conversationId: string): Promise<PeekInfo> {
// This can be undefined in two cases:
//

View file

@ -19,7 +19,10 @@ import type { BadgeType } from '../badges/types';
import type { LoggerType } from '../types/Logging';
import type { ReadStatus } from '../messages/MessageReadStatus';
import type { RawBodyRange } from '../types/BodyRange';
import type { GetMessagesBetweenOptions } from './Server';
import type {
GetMessagesBetweenOptions,
MaybeStaleCallHistory,
} from './Server';
import type { MessageTimestamps } from '../state/ducks/conversations';
import type {
CallHistoryDetails,
@ -665,6 +668,10 @@ export type DataInterface = {
conversationId: string,
eraId: string
) => Promise<boolean>;
markCallHistoryMissed(callIds: ReadonlyArray<string>): Promise<void>;
getRecentStaleRingsAndMarkOlderMissed(): Promise<
ReadonlyArray<MaybeStaleCallHistory>
>;
migrateConversationMessages: (
obsoleteId: string,
currentId: string

View file

@ -26,6 +26,7 @@ import {
map,
mapValues,
omit,
partition,
pick,
} from 'lodash';
@ -157,6 +158,8 @@ import {
CallHistoryFilterStatus,
callHistoryDetailsSchema,
CallDirection,
GroupCallStatus,
CallType,
} from '../types/CallDisposition';
type ConversationRow = Readonly<{
@ -316,6 +319,8 @@ const dataInterface: ServerInterface = {
getCallHistoryGroups,
saveCallHistory,
hasGroupCallHistoryMessage,
markCallHistoryMissed,
getRecentStaleRingsAndMarkOlderMissed,
migrateConversationMessages,
getMessagesBetween,
getNearbyMessageFromDeletedSet,
@ -3802,6 +3807,65 @@ async function hasGroupCallHistoryMessage(
return exists !== 0;
}
function _markCallHistoryMissed(db: Database, callIds: ReadonlyArray<string>) {
batchMultiVarQuery(db, callIds, batch => {
const [updateQuery, updateParams] = sql`
UPDATE callsHistory
SET status = ${sqlConstant(GroupCallStatus.Missed)}
WHERE callId IN (${sqlJoin(batch)})
`;
return db.prepare(updateQuery).run(updateParams);
});
}
async function markCallHistoryMissed(
callIds: ReadonlyArray<string>
): Promise<void> {
const db = await getWritableInstance();
return db.transaction(() => _markCallHistoryMissed(db, callIds))();
}
export type MaybeStaleCallHistory = Readonly<
Pick<CallHistoryDetails, 'callId' | 'peerId'>
>;
async function getRecentStaleRingsAndMarkOlderMissed(): Promise<
ReadonlyArray<MaybeStaleCallHistory>
> {
const db = await getWritableInstance();
return db.transaction(() => {
const [selectQuery, selectParams] = sql`
SELECT callId, peerId FROM callsHistory
WHERE
type = ${sqlConstant(CallType.Group)} AND
status = ${sqlConstant(GroupCallStatus.Ringing)}
ORDER BY timestamp DESC
`;
const ringingCalls = db.prepare(selectQuery).all(selectParams);
const seen = new Set<string>();
const [latestCalls, pastCalls] = partition(ringingCalls, result => {
if (seen.size >= 10) {
return false;
}
if (seen.has(result.peerId)) {
return false;
}
seen.add(result.peerId);
return true;
});
_markCallHistoryMissed(
db,
pastCalls.map(result => result.callId)
);
// These are returned so we can peek them.
return latestCalls;
})();
}
async function migrateConversationMessages(
obsoleteId: string,
currentId: string

View file

@ -0,0 +1,105 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { v4 as generateUuid } from 'uuid';
import { times } from 'lodash';
import dataInterface from '../../sql/Client';
import { CallMode } from '../../types/Calling';
import { generateAci } from '../../types/ServiceId';
import type { CallHistoryDetails } from '../../types/CallDisposition';
import {
CallDirection,
CallType,
GroupCallStatus,
} from '../../types/CallDisposition';
import type { MaybeStaleCallHistory } from '../../sql/Server';
const {
removeAll,
getRecentStaleRingsAndMarkOlderMissed,
saveCallHistory,
getAllCallHistory,
} = dataInterface;
describe('sql/getRecentStaleRingsAndMarkOlderMissed', () => {
beforeEach(async () => {
await removeAll();
});
const now = Date.now();
let offset = 0;
async function makeCall(
peerId: string,
callId: string,
status: GroupCallStatus
) {
const timestamp = now + offset;
offset += 1;
const call: CallHistoryDetails = {
callId,
peerId,
ringerId: generateAci(),
mode: CallMode.Group,
type: CallType.Group,
direction: CallDirection.Incoming,
timestamp,
status,
};
await saveCallHistory(call);
return call;
}
function toMissed(call: CallHistoryDetails) {
return { ...call, status: GroupCallStatus.Missed };
}
function toMaybeStale(call: CallHistoryDetails): MaybeStaleCallHistory {
return { callId: call.callId, peerId: call.peerId };
}
it('should mark every call but the latest with the same peer as missed', async () => {
const peer1 = generateUuid();
const peer2 = generateUuid();
const call1 = await makeCall(peer1, '1', GroupCallStatus.Ringing);
const call2 = await makeCall(peer1, '2', GroupCallStatus.Ringing);
const call3 = await makeCall(peer2, '3', GroupCallStatus.Ringing);
const call4 = await makeCall(peer2, '4', GroupCallStatus.Ringing);
const callsToCheck = await getRecentStaleRingsAndMarkOlderMissed();
const callHistory = await getAllCallHistory();
assert.deepEqual(callHistory, [
toMissed(call1),
call2, // latest peer1
toMissed(call3),
call4, // latest peer2
]);
assert.deepEqual(callsToCheck, [
// in order of timestamp
toMaybeStale(call4),
toMaybeStale(call2),
]);
});
it('should mark every ringing call after the first 10 as missed', async () => {
const calls = await Promise.all(
times(15, async i => {
return makeCall(generateUuid(), String(i), GroupCallStatus.Ringing);
})
);
const callsToCheck = await getRecentStaleRingsAndMarkOlderMissed();
const callHistory = await getAllCallHistory();
assert.deepEqual(callHistory, [
// first 10 are not missed
...calls.slice(0, -10).map(toMissed),
...calls.slice(-10),
]);
assert.deepEqual(
callsToCheck,
calls.slice(-10).map(toMaybeStale).reverse()
);
});
});