Cleanup stale ringing calls
This commit is contained in:
parent
a751eab67d
commit
e69826dcc6
4 changed files with 200 additions and 2 deletions
|
@ -351,7 +351,8 @@ export class CallingClass {
|
||||||
reduxInterface.setPresenting();
|
reduxInterface.setPresenting();
|
||||||
});
|
});
|
||||||
|
|
||||||
void this.cleanExpiredGroupCallRingsAndLoop();
|
drop(this.cleanExpiredGroupCallRingsAndLoop());
|
||||||
|
drop(this.cleanupStaleRingingCalls());
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
drop(this.enumerateMediaDevices());
|
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> {
|
public async peekGroupCall(conversationId: string): Promise<PeekInfo> {
|
||||||
// This can be undefined in two cases:
|
// This can be undefined in two cases:
|
||||||
//
|
//
|
||||||
|
|
|
@ -19,7 +19,10 @@ import type { BadgeType } from '../badges/types';
|
||||||
import type { LoggerType } from '../types/Logging';
|
import type { LoggerType } from '../types/Logging';
|
||||||
import type { ReadStatus } from '../messages/MessageReadStatus';
|
import type { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
import type { RawBodyRange } from '../types/BodyRange';
|
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 { MessageTimestamps } from '../state/ducks/conversations';
|
||||||
import type {
|
import type {
|
||||||
CallHistoryDetails,
|
CallHistoryDetails,
|
||||||
|
@ -665,6 +668,10 @@ export type DataInterface = {
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
eraId: string
|
eraId: string
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
|
markCallHistoryMissed(callIds: ReadonlyArray<string>): Promise<void>;
|
||||||
|
getRecentStaleRingsAndMarkOlderMissed(): Promise<
|
||||||
|
ReadonlyArray<MaybeStaleCallHistory>
|
||||||
|
>;
|
||||||
migrateConversationMessages: (
|
migrateConversationMessages: (
|
||||||
obsoleteId: string,
|
obsoleteId: string,
|
||||||
currentId: string
|
currentId: string
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
map,
|
map,
|
||||||
mapValues,
|
mapValues,
|
||||||
omit,
|
omit,
|
||||||
|
partition,
|
||||||
pick,
|
pick,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
|
|
||||||
|
@ -157,6 +158,8 @@ import {
|
||||||
CallHistoryFilterStatus,
|
CallHistoryFilterStatus,
|
||||||
callHistoryDetailsSchema,
|
callHistoryDetailsSchema,
|
||||||
CallDirection,
|
CallDirection,
|
||||||
|
GroupCallStatus,
|
||||||
|
CallType,
|
||||||
} from '../types/CallDisposition';
|
} from '../types/CallDisposition';
|
||||||
|
|
||||||
type ConversationRow = Readonly<{
|
type ConversationRow = Readonly<{
|
||||||
|
@ -316,6 +319,8 @@ const dataInterface: ServerInterface = {
|
||||||
getCallHistoryGroups,
|
getCallHistoryGroups,
|
||||||
saveCallHistory,
|
saveCallHistory,
|
||||||
hasGroupCallHistoryMessage,
|
hasGroupCallHistoryMessage,
|
||||||
|
markCallHistoryMissed,
|
||||||
|
getRecentStaleRingsAndMarkOlderMissed,
|
||||||
migrateConversationMessages,
|
migrateConversationMessages,
|
||||||
getMessagesBetween,
|
getMessagesBetween,
|
||||||
getNearbyMessageFromDeletedSet,
|
getNearbyMessageFromDeletedSet,
|
||||||
|
@ -3802,6 +3807,65 @@ async function hasGroupCallHistoryMessage(
|
||||||
return exists !== 0;
|
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(
|
async function migrateConversationMessages(
|
||||||
obsoleteId: string,
|
obsoleteId: string,
|
||||||
currentId: string
|
currentId: string
|
||||||
|
|
|
@ -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()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue