Return early in handleIncomingCall if call is ended

This commit is contained in:
Scott Nonnenberg
2025-03-07 03:27:24 +10:00
committed by GitHub
parent aff9a3213e
commit bf438e2456
2 changed files with 212 additions and 63 deletions

View File

@@ -197,6 +197,7 @@ type CallingReduxInterface = Pick<
| 'callStateChange' | 'callStateChange'
| 'cancelIncomingGroupCallRing' | 'cancelIncomingGroupCallRing'
| 'cancelPresenting' | 'cancelPresenting'
| 'declineCall'
| 'directCallAudioLevelsChange' | 'directCallAudioLevelsChange'
| 'groupCallAudioLevelsChange' | 'groupCallAudioLevelsChange'
| 'groupCallEnded' | 'groupCallEnded'
@@ -351,6 +352,46 @@ async function ensureSystemPermissions({
} }
} }
function getLogId(
options:
| {
source: string;
additional?: string;
conversationId: string | undefined;
}
| {
source: string;
additional?: string;
conversation: ConversationModel | undefined;
}
| {
source: string;
additional?: string;
conversationType: ConversationType | undefined;
}
): string {
const { source, additional } = options;
let idForLogging: string;
if ('conversationId' in options) {
idForLogging =
window.ConversationController.get(
options.conversationId
)?.idForLogging() ?? 'not found';
} else if ('conversation' in options) {
idForLogging = options.conversation?.idForLogging() ?? 'not found';
} else {
const { conversationType } = options;
idForLogging = conversationType
? getConversationIdForLogging(conversationType)
: 'not found';
}
const additionalText = additional ? `/${additional}` : '';
return `${source}(${idForLogging}${additionalText})`;
}
export class CallingClass { export class CallingClass {
readonly #videoCapturer: GumVideoCapturer; readonly #videoCapturer: GumVideoCapturer;
@@ -478,12 +519,18 @@ export class CallingClass {
} }
)) ))
> { > {
log.info('CallingClass.startCallingLobby()'); const logId = getLogId({
source: 'CallingClass.startCallingLobby',
conversationType: conversation,
});
log.info(logId);
const callMode = getConversationCallMode(conversation); const callMode = getConversationCallMode(conversation);
switch (callMode) { switch (callMode) {
case null: case null:
log.error('Conversation does not support calls, new call not allowed.'); log.error(
`${logId}: Conversation does not support calls, new call not allowed.`
);
return; return;
case CallMode.Direct: { case CallMode.Direct: {
const conversationModel = window.ConversationController.get( const conversationModel = window.ConversationController.get(
@@ -493,7 +540,9 @@ export class CallingClass {
!conversationModel || !conversationModel ||
!this.#getRemoteUserIdFromConversation(conversationModel) !this.#getRemoteUserIdFromConversation(conversationModel)
) { ) {
log.error('Missing remote user identifier, new call not allowed.'); log.error(
`${logId}: Missing remote user identifier, new call not allowed.`
);
return; return;
} }
break; break;
@@ -502,7 +551,7 @@ export class CallingClass {
break; break;
case CallMode.Adhoc: case CallMode.Adhoc:
log.error( log.error(
'startCallingLobby() not implemented for adhoc calls. Did you mean: startCallLinkLobby()?' `${logId}: not implemented for adhoc calls. Did you mean: startCallLinkLobby()?`
); );
return; return;
default: default:
@@ -510,22 +559,24 @@ export class CallingClass {
} }
if (!this.#reduxInterface) { if (!this.#reduxInterface) {
log.error('Missing uxActions, new call not allowed.'); log.error(`${logId}: Missing uxActions, new call not allowed.`);
return; return;
} }
if (!this.#localDeviceId) { if (!this.#localDeviceId) {
log.error('Missing local device identifier, new call not allowed.'); log.error(
`${logId}: Missing local device identifier, new call not allowed.`
);
return; return;
} }
const haveMediaPermissions = await this.#requestPermissions(hasLocalVideo); const haveMediaPermissions = await this.#requestPermissions(hasLocalVideo);
if (!haveMediaPermissions) { if (!haveMediaPermissions) {
log.info('Permissions were denied, new call not allowed.'); log.info(`${logId}: Permissions were denied, new call not allowed.`);
return; return;
} }
log.info('CallingClass.startCallingLobby(): Starting lobby'); log.info(`${logId}: Starting lobby`);
await ensureSystemPermissions({ hasLocalAudio, hasLocalVideo }); await ensureSystemPermissions({ hasLocalAudio, hasLocalVideo });
// It's important that this function comes before any calls to // It's important that this function comes before any calls to
@@ -558,6 +609,8 @@ export class CallingClass {
'Expected local audio to be enabled for direct call lobbies' 'Expected local audio to be enabled for direct call lobbies'
); );
enableLocalCameraIfNecessary(); enableLocalCameraIfNecessary();
log.info(`${logId}: Returning direct call`);
return { return {
callMode: CallMode.Direct, callMode: CallMode.Direct,
hasLocalAudio, hasLocalAudio,
@@ -570,7 +623,7 @@ export class CallingClass {
!conversation.secretParams !conversation.secretParams
) { ) {
log.error( log.error(
'Conversation is missing required parameters. Cannot connect group call' `${logId}: Conversation is missing required parameters. Cannot connect group call`
); );
return; return;
} }
@@ -585,6 +638,7 @@ export class CallingClass {
enableLocalCameraIfNecessary(); enableLocalCameraIfNecessary();
log.info(`${logId}: Returning group call`);
return { return {
callMode: CallMode.Group, callMode: CallMode.Group,
...this.#formatGroupCallForRedux(groupCall), ...this.#formatGroupCallForRedux(groupCall),
@@ -596,6 +650,12 @@ export class CallingClass {
} }
stopCallingLobby(conversationId?: string): void { stopCallingLobby(conversationId?: string): void {
const logId = getLogId({
source: 'CallingClass.stopCallingLobby',
conversationId,
});
log.info(logId);
this.disableLocalVideo(); this.disableLocalVideo();
this.#stopDeviceReselectionTimer(); this.#stopDeviceReselectionTimer();
this.#lastMediaDeviceSettings = undefined; this.#lastMediaDeviceSettings = undefined;
@@ -696,6 +756,7 @@ export class CallingClass {
const sfuUrl = this._sfuUrl; const sfuUrl = this._sfuUrl;
const logId = `deleteCallLink(${callLink.roomId})`; const logId = `deleteCallLink(${callLink.roomId})`;
log.info(logId);
const callLinkRootKey = CallLinkRootKey.parse(callLink.rootKey); const callLinkRootKey = CallLinkRootKey.parse(callLink.rootKey);
strictAssert(callLink.adminKey, 'Missing admin key'); strictAssert(callLink.adminKey, 'Missing admin key');
@@ -813,6 +874,8 @@ export class CallingClass {
const roomId = getRoomIdFromRootKey(callLinkRootKey); const roomId = getRoomIdFromRootKey(callLinkRootKey);
const logId = `readCallLink(${roomId})`; const logId = `readCallLink(${roomId})`;
log.info(logId);
const authCredentialPresentation = const authCredentialPresentation =
await getCallLinkAuthCredentialPresentation(callLinkRootKey); await getCallLinkAuthCredentialPresentation(callLinkRootKey);
@@ -897,8 +960,7 @@ export class CallingClass {
return; return;
} }
const idForLogging = getConversationIdForLogging(conversation.attributes); const logId = getLogId({ source: 'startOutgoingDirectCall', conversation });
const logId = `startOutgoingDirectCall(${idForLogging}`;
log.info(logId); log.info(logId);
if (!this.#reduxInterface) { if (!this.#reduxInterface) {
@@ -1144,6 +1206,9 @@ export class CallingClass {
throw new Error('Missing SFU URL; not connecting group call'); throw new Error('Missing SFU URL; not connecting group call');
} }
const logId = getLogId({ source: 'connectGroupCall', conversationId });
log.info(logId);
const groupIdBuffer = Buffer.from(Bytes.fromBase64(groupId)); const groupIdBuffer = Buffer.from(Bytes.fromBase64(groupId));
let isRequestingMembershipProof = false; let isRequestingMembershipProof = false;
@@ -1171,7 +1236,7 @@ export class CallingClass {
); );
} }
} catch (err) { } catch (err) {
log.error('Failed to fetch membership proof', err); log.error(`${logId}: Failed to fetch membership proof`, err);
} finally { } finally {
isRequestingMembershipProof = false; isRequestingMembershipProof = false;
} }
@@ -1182,7 +1247,9 @@ export class CallingClass {
if (!outerGroupCall) { if (!outerGroupCall) {
// This should be very rare, likely due to RingRTC not being able to get a lock // This should be very rare, likely due to RingRTC not being able to get a lock
// or memory or something like that. // or memory or something like that.
throw new Error('Failed to get a group call instance; cannot start call'); throw new Error(
`${logId} Failed to get a group call instance; cannot start call`
);
} }
outerGroupCall.connect(); outerGroupCall.connect();
@@ -1215,8 +1282,13 @@ export class CallingClass {
return existing; return existing;
} }
const logId = `connectCallLinkCall(${roomId}`;
log.info(logId);
if (!this._sfuUrl) { if (!this._sfuUrl) {
throw new Error('Missing SFU URL; not connecting group call link call'); throw new Error(
`${logId}: Missing SFU URL; not connecting group call link call`
);
} }
const outerGroupCall = RingRTC.getCallLinkCall( const outerGroupCall = RingRTC.getCallLinkCall(
@@ -1232,7 +1304,9 @@ export class CallingClass {
if (!outerGroupCall) { if (!outerGroupCall) {
// This should be very rare, likely due to RingRTC not being able to get a lock // This should be very rare, likely due to RingRTC not being able to get a lock
// or memory or something like that. // or memory or something like that.
throw new Error('Failed to get a group call instance; cannot start call'); throw new Error(
`${logId}: Failed to get a group call instance; cannot start call`
);
} }
outerGroupCall.connect(); outerGroupCall.connect();
@@ -1249,21 +1323,20 @@ export class CallingClass {
hasLocalVideo: boolean, hasLocalVideo: boolean,
shouldRing: boolean shouldRing: boolean
): Promise<void> { ): Promise<void> {
const conversation = const conversation = window.ConversationController.get(conversationId);
window.ConversationController.get(conversationId)?.format();
if (!conversation) { if (!conversation) {
log.error('Missing conversation; not joining group call'); log.error('joinGroupCall: Missing conversation; not joining group call');
return; return;
} }
const logId = `joinGroupCall(${getConversationIdForLogging(conversation)})`; const logId = getLogId({
source: 'joinGroupCall',
conversation,
});
log.info(logId); log.info(logId);
if ( const { groupId, publicParams, secretParams } = conversation.attributes;
!conversation.groupId || if (!groupId || !publicParams || !secretParams) {
!conversation.publicParams ||
!conversation.secretParams
) {
log.error( log.error(
`${logId}: Conversation is missing required parameters. Cannot join group call` `${logId}: Conversation is missing required parameters. Cannot join group call`
); );
@@ -1281,9 +1354,9 @@ export class CallingClass {
await this.#startDeviceReselectionTimer(); await this.#startDeviceReselectionTimer();
const groupCall = this.connectGroupCall(conversationId, { const groupCall = this.connectGroupCall(conversationId, {
groupId: conversation.groupId, groupId,
publicParams: conversation.publicParams, publicParams,
secretParams: conversation.secretParams, secretParams,
}); });
groupCall.setOutgoingAudioMuted(!hasLocalAudio); groupCall.setOutgoingAudioMuted(!hasLocalAudio);
@@ -1567,6 +1640,8 @@ export class CallingClass {
} }
const logId = `sendProfileKeysForAdhocCall aci=${aci}`; const logId = `sendProfileKeysForAdhocCall aci=${aci}`;
log.info(logId);
const conversation = window.ConversationController.lookupOrCreate({ const conversation = window.ConversationController.lookupOrCreate({
serviceId: aci, serviceId: aci,
reason, reason,
@@ -1925,7 +2000,10 @@ export class CallingClass {
return false; return false;
} }
const logId = `sendGroupCallUpdateMessage/${conversation.idForLogging()}`; const logId = getLogId({
source: 'sendGroupCallUpdateMessage',
conversation,
});
const groupV2 = conversation.getGroupV2Info(); const groupV2 = conversation.getGroupV2Info();
if (!groupV2) { if (!groupV2) {
@@ -1954,11 +2032,15 @@ export class CallingClass {
conversationId: string, conversationId: string,
asVideoCall: boolean asVideoCall: boolean
): Promise<void> { ): Promise<void> {
log.info('CallingClass.acceptDirectCall()'); const logId = getLogId({
source: 'CallingClass.acceptDirectCall',
conversationId,
});
log.info(logId);
const callId = this.#getCallIdForConversation(conversationId); const callId = this.#getCallIdForConversation(conversationId);
if (!callId) { if (!callId) {
log.warn('Trying to accept a non-existent call'); log.warn(`${logId}: Trying to accept a non-existent call`);
return; return;
} }
@@ -1973,17 +2055,23 @@ export class CallingClass {
RingRTC.setVideoRenderer(callId, this.videoRenderer); RingRTC.setVideoRenderer(callId, this.videoRenderer);
RingRTC.accept(callId, asVideoCall); RingRTC.accept(callId, asVideoCall);
} else { } else {
log.info('Permissions were denied, call not allowed, hanging up.'); log.info(
`${logId}: Permissions were denied, call not allowed, hanging up.`
);
RingRTC.hangup(callId); RingRTC.hangup(callId);
} }
} }
declineDirectCall(conversationId: string): void { declineDirectCall(conversationId: string): void {
log.info('CallingClass.declineDirectCall()'); const logId = getLogId({
source: 'CallingClass.declineDirectCall',
conversationId,
});
log.info(logId);
const callId = this.#getCallIdForConversation(conversationId); const callId = this.#getCallIdForConversation(conversationId);
if (!callId) { if (!callId) {
log.warn('declineDirectCall: Trying to decline a non-existent call'); log.warn(`${logId}: Trying to decline a non-existent call`);
return; return;
} }
@@ -1991,7 +2079,11 @@ export class CallingClass {
} }
declineGroupCall(conversationId: string, ringId: bigint): void { declineGroupCall(conversationId: string, ringId: bigint): void {
log.info('CallingClass.declineGroupCall()'); const logId = getLogId({
source: 'CallingClass.declineGroupCall',
conversationId,
});
log.info(logId);
const groupId = const groupId =
window.ConversationController.get(conversationId)?.get('groupId'); window.ConversationController.get(conversationId)?.get('groupId');
@@ -2011,13 +2103,16 @@ export class CallingClass {
} }
hangup(conversationId: string, reason: string): void { hangup(conversationId: string, reason: string): void {
log.info(`CallingClass.hangup(${conversationId}): ${reason}`); const logId = getLogId({
source: 'CallingClass.hangup',
conversationId,
additional: reason,
});
log.info(logId);
const specificCall = getOwn(this.#callsLookup, conversationId); const specificCall = getOwn(this.#callsLookup, conversationId);
if (!specificCall) { if (!specificCall) {
log.error( log.error(`${logId}: Trying to hang up a non-existent call`);
`hangup: Trying to hang up a non-existent call for conversation ${conversationId}`
);
} }
ipcRenderer.send( ipcRenderer.send(
@@ -2026,10 +2121,10 @@ export class CallingClass {
); );
const entries = Object.entries(this.#callsLookup); const entries = Object.entries(this.#callsLookup);
log.info(`hangup: ${entries.length} call(s) to hang up...`); log.info(`${logId}: ${entries.length} call(s) to hang up...`);
entries.forEach(([callConversationId, call]) => { entries.forEach(([callConversationId, call]) => {
log.info(`hangup: Hanging up conversation ${callConversationId}`); log.info(`${logId}: Hanging up conversation ${callConversationId}`);
if (call instanceof Call) { if (call instanceof Call) {
RingRTC.hangup(call.callId); RingRTC.hangup(call.callId);
} else if (call instanceof GroupCall) { } else if (call instanceof GroupCall) {
@@ -2042,7 +2137,7 @@ export class CallingClass {
} }
}); });
log.info('hangup: Done.'); log.info(`${logId}: Done.`);
} }
hangupAllCalls(reason: string): void { hangupAllCalls(reason: string): void {
@@ -2502,6 +2597,7 @@ export class CallingClass {
callingMessage: Proto.ICallMessage callingMessage: Proto.ICallMessage
): Promise<void> { ): Promise<void> {
const logId = `CallingClass.handleCallingMessage(${envelope.timestamp})`; const logId = `CallingClass.handleCallingMessage(${envelope.timestamp})`;
log.info(logId);
const enableIncomingCalls = window.Events.getIncomingCallNotification(); const enableIncomingCalls = window.Events.getIncomingCallNotification();
if (callingMessage.offer && !enableIncomingCalls) { if (callingMessage.offer && !enableIncomingCalls) {
@@ -2793,7 +2889,12 @@ export class CallingClass {
}); });
} }
const logId = `handleGroupCallRingUpdate(${conversation.idForLogging()})`; const logId = getLogId({
source: 'CallingClass.handleGroupCallRingUpdate',
conversation,
});
log.info(logId);
if (conversation.isBlocked()) { if (conversation.isBlocked()) {
log.warn(`${logId}: is blocked`); log.warn(`${logId}: is blocked`);
return; return;
@@ -2928,23 +3029,29 @@ export class CallingClass {
// If we return null here, we hang up the call. // If we return null here, we hang up the call.
async #handleIncomingCall(call: Call): Promise<boolean> { async #handleIncomingCall(call: Call): Promise<boolean> {
log.info('CallingClass.handleIncomingCall()');
if (!this.#reduxInterface || !this.#localDeviceId) { if (!this.#reduxInterface || !this.#localDeviceId) {
log.error('Missing required objects, ignoring incoming call.'); log.error(
'handleIncomingCall: Missing required objects, ignoring incoming call.'
);
return false; return false;
} }
const conversation = window.ConversationController.get(call.remoteUserId); const conversation = window.ConversationController.get(call.remoteUserId);
if (!conversation) { if (!conversation) {
log.error('Missing conversation, ignoring incoming call.'); log.error(
'handleIncomingCall: Missing conversation, ignoring incoming call.'
);
return false; return false;
} }
const logId = getLogId({
source: 'CallingClass.handleIncomingCall',
conversation,
});
log.info(logId);
if (conversation.isBlocked()) { if (conversation.isBlocked()) {
log.warn( log.warn(`${logId}: ${conversation.idForLogging()} is blocked`);
`handleIncomingCall(): ${conversation.idForLogging()} is blocked`
);
return false; return false;
} }
try { try {
@@ -2956,9 +3063,7 @@ export class CallingClass {
verifiedEnum === verifiedEnum ===
window.textsecure.storage.protocol.VerifiedStatus.UNVERIFIED window.textsecure.storage.protocol.VerifiedStatus.UNVERIFIED
) { ) {
log.info( log.info(`${logId}: Peer is not trusted, ignoring incoming call`);
`Peer is not trusted, ignoring incoming call for conversation: ${conversation.idForLogging()}`
);
const localCallEvent = LocalCallEvent.Missed; const localCallEvent = LocalCallEvent.Missed;
const peerId = getPeerIdFromConversation(conversation.attributes); const peerId = getPeerIdFromConversation(conversation.attributes);
@@ -2973,6 +3078,14 @@ export class CallingClass {
return false; return false;
} }
if (call.endedReason) {
log.warn(
`${logId}: Returning early, call ended with reason ${call.endedReason}`
);
this.#reduxInterface?.declineCall({ conversationId: conversation.id });
return false;
}
this.#attachToCall(conversation, call); this.#attachToCall(conversation, call);
this.#reduxInterface.receiveIncomingDirectCall({ this.#reduxInterface.receiveIncomingDirectCall({
@@ -2980,9 +3093,10 @@ export class CallingClass {
isVideoCall: call.isVideoCall, isVideoCall: call.isVideoCall,
}); });
log.warn(`${logId}: Returning true`);
return true; return true;
} catch (err) { } catch (err) {
log.error(`Ignoring incoming call: ${Errors.toLogFormat(err)}`); log.error(`${logId}: Ignoring incoming call: ${Errors.toLogFormat(err)}`);
return false; return false;
} }
} }
@@ -2998,9 +3112,16 @@ export class CallingClass {
) { ) {
const conversation = window.ConversationController.get(remoteUserId); const conversation = window.ConversationController.get(remoteUserId);
if (!conversation) { if (!conversation) {
log.warn('handleAutoEndedIncomingCallRequest: Conversation not found');
return; return;
} }
const logId = getLogId({
source: 'handleAutoEndedIncomingCallRequest',
conversation,
});
log.info(logId);
const callId = Long.fromValue(callIdValue).toString(); const callId = Long.fromValue(callIdValue).toString();
const peerId = getPeerIdFromConversation(conversation.attributes); const peerId = getPeerIdFromConversation(conversation.attributes);
@@ -3028,9 +3149,7 @@ export class CallingClass {
); );
if (!this.#reduxInterface) { if (!this.#reduxInterface) {
log.error( log.error(`${logId}: Unable to update redux for call`);
'handleAutoEndedIncomingCallRequest: Unable to update redux for call'
);
} }
this.#reduxInterface?.callStateChange({ this.#reduxInterface?.callStateChange({
acceptedTime: null, acceptedTime: null,
@@ -3055,6 +3174,12 @@ export class CallingClass {
return; return;
} }
const logId = getLogId({
source: 'CallingClass.attachToCall',
conversation,
});
log.info(logId);
let acceptedTime: number | null = null; let acceptedTime: number | null = null;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@@ -3263,21 +3388,45 @@ export class CallingClass {
} }
if (!window.textsecure.messaging) { if (!window.textsecure.messaging) {
log.error('handleStartCall: offline!'); log.error('CallingClass.handleStartCall: offline!');
return false;
}
const conversation = window.ConversationController.get(call.remoteUserId);
if (!conversation) {
log.error(
'CallingClass.handleStartCall: Missing conversation, ignoring incoming call.'
);
return false;
}
const logId = getLogId({
source: 'CallingClass.handleStartCall',
conversation,
});
log.info(logId);
if (call.endedReason) {
log.warn(
`${logId}: Returning early, call ended with reason ${call.endedReason}`
);
this.#reduxInterface?.declineCall({ conversationId: conversation.id });
return false; return false;
} }
const iceServerConfig = const iceServerConfig =
await window.textsecure.messaging.server.getIceServers(); await window.textsecure.messaging.server.getIceServers();
const shouldRelayCalls = window.Events.getAlwaysRelayCalls(); // We do this again, since getIceServers is a call that can take some time
if (call.endedReason) {
const conversation = window.ConversationController.get(call.remoteUserId); log.warn(
if (!conversation) { `${logId}: Returning early, call ended with reason ${call.endedReason}`
log.error('Missing conversation, ignoring incoming call.'); );
this.#reduxInterface?.declineCall({ conversationId: conversation.id });
return false; return false;
} }
const shouldRelayCalls = window.Events.getAlwaysRelayCalls();
// If the peer is not a Signal Connection, force IP hiding. // If the peer is not a Signal Connection, force IP hiding.
const isContactUntrusted = !isSignalConnection(conversation.attributes); const isContactUntrusted = !isSignalConnection(conversation.attributes);

View File

@@ -1284,7 +1284,7 @@ function declineCall(
const call = getOwn(getState().calling.callsByConversation, conversationId); const call = getOwn(getState().calling.callsByConversation, conversationId);
if (!call) { if (!call) {
log.error('Trying to decline a non-existent call'); log.warn('Trying to decline a non-existent call');
return; return;
} }