Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

266 lines
8.8 KiB

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import getCallAudioAsset, { CallAudioAssetName } from "../../components/call/getAudioAsset";
import { MOUNT_CLASS_TO } from "../../config/debug";
import IS_CALL_SUPPORTED from "../../environment/callSupport";
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
import insertInDescendSortedArray from "../../helpers/array/insertInDescendSortedArray";
import AudioAssetPlayer from "../../helpers/audioAssetPlayer";
import bytesCmp from "../../helpers/bytes/bytesCmp";
import EventListenerBase from "../../helpers/eventListenerBase";
import tsNow from "../../helpers/tsNow";
import { PhoneCallProtocol } from "../../layer";
import { CallId } from "../appManagers/appCallsManager";
import { AppManagers } from "../appManagers/managers";
import { logger } from "../logger";
import apiManagerProxy from "../mtproto/mtprotoworker";
import { NULL_PEER_ID } from "../mtproto/mtproto_config";
import rootScope from "../rootScope";
import CallInstance from "./callInstance";
import CALL_STATE from "./callState";
const CALL_REQUEST_TIMEOUT = 45e3;
export class CallsController extends EventListenerBase<{
instance: (details: {hasCurrent: boolean, instance: CallInstance}) => void,
accepting: (instance: CallInstance) => void, // это костыль. используется при параллельном вызове, чтобы заменить звонок в topbarCall
incompatible: (userId: UserId) => void,
}> {
private log: ReturnType<typeof logger>;
private managers: AppManagers;
private audioAsset: AudioAssetPlayer<CallAudioAssetName>;
private instances: Map<CallId, CallInstance>;
private sortedInstances: Array<CallInstance>;
private tempId: number;
public construct(managers: AppManagers) {
this.managers = managers;
this.log = logger('CC');
if(!IS_CALL_SUPPORTED) {
return;
}
this.audioAsset = getCallAudioAsset();
this.tempId = 0;
this.instances = new Map();
this.sortedInstances = [];
rootScope.addEventListener('call_update', async(call) => {
let instance = this.instances.get(call.id);
if(instance) {
instance.setPhoneCall(call);
}
switch(call._) {
case 'phoneCallDiscarded': {
if(instance) {
instance.hangUp(call.reason?._, true);
}
break;
}
case 'phoneCallAccepted': {
if(instance) {
/* if(!this.verifyProtocolCompatibility(call.protocol)) {
instance.hangUp('phoneCallDiscardReasonDisconnect');
rootScope.dispatchEvent('call_incompatible', instance.interlocutorUserId);
break;
} */
instance.confirmCall();
}
break;
}
case 'phoneCallRequested': {
if(!instance) {
/* if(!this.verifyProtocolCompatibility(call.protocol)) {
rootScope.dispatchEvent('call_incompatible', call.admin_id);
break;
} */
instance = this.createCallInstance({
isOutgoing: false,
interlocutorUserId: call.admin_id
});
instance.overrideConnectionState(CALL_STATE.PENDING);
instance.setPhoneCall(call);
instance.setHangUpTimeout(CALL_REQUEST_TIMEOUT, 'phoneCallDiscardReasonMissed');
}
break;
}
case 'phoneCall': {
if(!instance || instance.encryptionKey) {
break;
}
const g_a = instance.dh.g_a = call.g_a_or_b;
const dh = instance.dh;
const g_a_hash = await apiManagerProxy.invokeCrypto('sha256', g_a);
if(!bytesCmp(dh.g_a_hash, g_a_hash)) {
this.log.error('Incorrect g_a_hash', dh.g_a_hash, g_a_hash);
break;
}
const {key, key_fingerprint} = await this.managers.appCallsManager.computeKey(g_a, dh.b, dh.p);
if(call.key_fingerprint !== key_fingerprint) {
this.log.error('Incorrect key fingerprint', call.key_fingerprint, key_fingerprint);
break;
}
instance.encryptionKey = key;
instance.joinCall();
break;
}
}
});
rootScope.addEventListener('call_signaling', ({callId, data}) => {
const instance = this.instances.get(callId);
if(instance?.id !== callId) {
return;
}
instance.onUpdatePhoneCallSignalingData(data);
});
}
public get currentCall() {
return this.sortedInstances[0];
}
public getCallByUserId(userId: UserId) {
for(const [callId, instance] of this.instances) {
if(instance.interlocutorUserId === userId) {
return instance;
}
}
}
private createCallInstance(options: {
isOutgoing: boolean,
interlocutorUserId: UserId,
protocol?: PhoneCallProtocol
}) {
const call = new CallInstance({
managers: this.managers,
...options,
});
call.addEventListener('state', (state) => {
const currentCall = this.currentCall;
if(state === CALL_STATE.CLOSED) {
this.instances.delete(call.id);
indexOfAndSplice(this.sortedInstances, call);
} else {
insertInDescendSortedArray(this.sortedInstances, call, 'sortIndex');
}
if(state === CALL_STATE.EXCHANGING_KEYS) {
call.wasTryingToJoin = true;
}
const hasConnected = call.connectedAt !== undefined;
if(state === CALL_STATE.EXCHANGING_KEYS || (state === CALL_STATE.CONNECTING && hasConnected)) {
call.setHangUpTimeout(CALL_REQUEST_TIMEOUT, 'phoneCallDiscardReasonDisconnect');
} else {
call.clearHangUpTimeout();
}
if(currentCall === call || !currentCall) {
if(state === CALL_STATE.CLOSED) {
if(!call.isOutgoing && !call.wasTryingToJoin) { // incoming call has been accepted on other device or ended
this.audioAsset.stopSound();
} else if(call.wasTryingToJoin && !hasConnected) { // something has happened during the key exchanging
this.audioAsset.playSound('voip_failed.mp3');
} else {
this.audioAsset.playSound(call.discardReason === 'phoneCallDiscardReasonBusy' ? 'call_busy.mp3' : 'call_end.mp3');
}
} else if(state === CALL_STATE.PENDING) {
this.audioAsset.playSound(call.isOutgoing ? 'call_outgoing.mp3' : 'call_incoming.mp3', true);
} else if(state === CALL_STATE.EXCHANGING_KEYS) {
this.audioAsset.playSoundIfDifferent('call_connect.mp3');
} else if(state === CALL_STATE.CONNECTING) {
if(call.duration) {
this.audioAsset.playSound('voip_connecting.mp3', true);
}
} else {
this.audioAsset.stopSound();
}
}
});
call.addEventListener('id', (id, prevId) => {
if(prevId !== undefined) {
this.instances.delete(prevId);
}
const hasCurrent = !!this.currentCall;
this.instances.set(id, call);
if(prevId === undefined) {
this.dispatchEvent('instance', {instance: call, hasCurrent: hasCurrent});
}
});
return call;
}
public async startCallInternal(userId: UserId, isVideo: boolean) {
this.log('p2pStartCallInternal', userId, isVideo);
const fullInfo = await this.managers.appProfileManager.getProfile(userId);
if(!fullInfo) return;
const {video_calls_available} = fullInfo.pFlags;
const call = this.createCallInstance({
isOutgoing: true,
interlocutorUserId: userId
});
call.requestInputSource(true, !!(isVideo && video_calls_available), false);
call.overrideConnectionState(CALL_STATE.REQUESTING);
call.setPhoneCall({
_: 'phoneCallWaiting',
access_hash: '',
admin_id: NULL_PEER_ID,
date: tsNow(true),
id: --this.tempId,
participant_id: userId,
protocol: call.protocol,
pFlags: {
video: isVideo || undefined
}
});
// return;
this.managers.appCallsManager.generateDh().then(async(dh) => {
call.dh = dh;
return this.managers.appCallsManager.requestCall(userId, call.protocol, call.dh.g_a_hash, isVideo && video_calls_available);
}).then((phoneCall) => {
call.overrideConnectionState(CALL_STATE.PENDING);
call.setPhoneCall(phoneCall);
call.setHangUpTimeout(CALL_REQUEST_TIMEOUT, 'phoneCallDiscardReasonHangup');
});
}
}
const callsController = new CallsController();
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.callsController = callsController);
export default callsController;