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.
384 lines
13 KiB
384 lines
13 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
* |
|
* Originally from: |
|
* https://github.com/evgeny-nadymov/telegram-react |
|
* Copyright (C) 2018 Evgeny Nadymov |
|
* https://github.com/evgeny-nadymov/telegram-react/blob/master/LICENSE |
|
*/ |
|
|
|
import type GroupCallConnectionInstance from '../calls/groupCallConnectionInstance'; |
|
import safeReplaceObject from '../../helpers/object/safeReplaceObject'; |
|
import {nextRandomUint} from '../../helpers/random'; |
|
import {DataJSON, GroupCall, GroupCallParticipant, GroupCallParticipantVideoSourceGroup, InputGroupCall, PhoneJoinGroupCall, PhoneJoinGroupCallPresentation, Update, Updates} from '../../layer'; |
|
import {logger} from '../logger'; |
|
import {NULL_PEER_ID} from '../mtproto/mtproto_config'; |
|
import {AppManager} from './manager'; |
|
import getPeerId from './utils/peers/getPeerId'; |
|
|
|
export type GroupCallId = GroupCall['id']; |
|
export type MyGroupCall = GroupCall | InputGroupCall; |
|
|
|
export type GroupCallConnectionType = 'main' | 'presentation'; |
|
|
|
export type JoinGroupCallJsonPayload = { |
|
fingerprints: { |
|
fingerprint: string; |
|
setup: string; |
|
hash: string; |
|
}[]; |
|
pwd: string; |
|
ssrc: number; |
|
'ssrc-groups': GroupCallParticipantVideoSourceGroup.groupCallParticipantVideoSourceGroup[]; |
|
ufrag: string; |
|
}; |
|
|
|
const GET_PARTICIPANTS_LIMIT = 100; |
|
|
|
export type GroupCallOutputSource = 'main' | 'presentation' | number; |
|
|
|
export class AppGroupCallsManager extends AppManager { |
|
private log: ReturnType<typeof logger>; |
|
|
|
private groupCalls: Map<GroupCallId, MyGroupCall>; |
|
private participants: Map<GroupCallId, Map<PeerId, GroupCallParticipant>>; |
|
private nextOffsets: Map<GroupCallId, string>; |
|
|
|
// private doNotDispatchParticipantUpdate: PeerId; |
|
|
|
protected after() { |
|
this.log = logger('GROUP-CALLS'); |
|
|
|
this.groupCalls = new Map(); |
|
this.participants = new Map(); |
|
this.nextOffsets = new Map(); |
|
|
|
this.apiUpdatesManager.addMultipleEventsListeners({ |
|
updateGroupCall: (update) => { |
|
this.saveGroupCall(update.call, update.chat_id); |
|
}, |
|
|
|
updateGroupCallParticipants: (update) => { |
|
this.saveGroupCall(update.call); |
|
|
|
// this.getGroupCallFull(update.call.id, true); // ! WARNING TEMP |
|
|
|
const groupCallId = update.call.id; |
|
this.saveApiParticipants(groupCallId, update.participants); |
|
} |
|
}); |
|
|
|
this.rootScope.addEventListener('group_call_update', (groupCall) => { |
|
if(groupCall._ === 'groupCallDiscarded') { |
|
this.participants.delete(groupCall.id); |
|
} |
|
}); |
|
} |
|
|
|
public getCachedParticipants(groupCallId: GroupCallId) { |
|
let participants = this.participants.get(groupCallId); |
|
if(!participants) { |
|
this.participants.set(groupCallId, participants = new Map()); |
|
} |
|
|
|
return participants; |
|
} |
|
|
|
private prepareToSavingNextOffset(groupCallId: GroupCallId) { |
|
const nextOffsetsMap = this.nextOffsets; |
|
|
|
const setNextOffset = (newNextOffset: string) => { |
|
if(nextOffsetsMap.get(groupCallId) === nextOffset) { |
|
nextOffsetsMap.set(groupCallId, newNextOffset); |
|
} |
|
}; |
|
|
|
const nextOffset = nextOffsetsMap.get(groupCallId); |
|
return { |
|
nextOffset, |
|
setNextOffset |
|
}; |
|
} |
|
|
|
public saveApiParticipant(groupCallId: GroupCallId, participant: GroupCallParticipant, skipCounterUpdating?: boolean) { |
|
const participants = this.getCachedParticipants(groupCallId); |
|
|
|
const peerId = getPeerId(participant.peer); |
|
|
|
const oldParticipant = participants.get(peerId); |
|
const hasLeft = participant.pFlags.left; |
|
if(!oldParticipant && hasLeft) { |
|
return; |
|
} |
|
|
|
// * fix missing flag |
|
if(!participant.pFlags.muted && !participant.pFlags.can_self_unmute) { |
|
participant.pFlags.can_self_unmute = true; |
|
} |
|
|
|
if(oldParticipant) { |
|
safeReplaceObject(oldParticipant, participant); |
|
participant = oldParticipant; |
|
} else { |
|
participants.set(peerId, participant); |
|
} |
|
|
|
// if(!skipCounterUpdating) { |
|
const groupCall = this.getGroupCall(groupCallId); |
|
if(groupCall?._ === 'groupCall') { |
|
let modified = false; |
|
if(hasLeft) { |
|
--groupCall.participants_count; |
|
modified = true; |
|
} else if(participant.pFlags.just_joined && !oldParticipant && !participant.pFlags.self) { |
|
++groupCall.participants_count; |
|
modified = true; |
|
} |
|
|
|
if(modified) { |
|
this.rootScope.dispatchEvent('group_call_update', groupCall); |
|
} |
|
} |
|
// } |
|
|
|
if(hasLeft) { |
|
participants.delete(peerId); |
|
} |
|
|
|
if(oldParticipant || true/* && this.doNotDispatchParticipantUpdate !== peerId */) { |
|
this.rootScope.dispatchEvent('group_call_participant', { |
|
groupCallId, |
|
participant |
|
}); |
|
} |
|
} |
|
|
|
public saveApiParticipants(groupCallId: GroupCallId, apiParticipants: GroupCallParticipant[], skipCounterUpdating?: boolean) { |
|
if((apiParticipants as any).saved) return; |
|
(apiParticipants as any).saved = true; |
|
apiParticipants.forEach((p) => this.saveApiParticipant(groupCallId, p, skipCounterUpdating)); |
|
} |
|
|
|
public async editParticipant(groupCallId: GroupCallId, participant: GroupCallParticipant, options: Partial<{ |
|
muted: boolean, |
|
volume: number, |
|
raiseHand: boolean, |
|
videoStopped: boolean, |
|
videoPaused: boolean, |
|
presentationPaused: boolean |
|
}>) { |
|
this.saveApiParticipant(groupCallId, participant); |
|
|
|
const peerId = participant.pFlags.self ? NULL_PEER_ID : getPeerId(participant.peer); |
|
const updates = await this.apiManager.invokeApiSingle('phone.editGroupCallParticipant', { |
|
call: this.getGroupCallInput(groupCallId), |
|
participant: peerId === NULL_PEER_ID ? this.appPeersManager.getInputPeerSelf() : this.appPeersManager.getInputPeerById(peerId), |
|
muted: options.muted, |
|
volume: options.volume, |
|
raise_hand: options.raiseHand, |
|
video_paused: options.videoPaused, |
|
video_stopped: options.videoStopped, |
|
presentation_paused: options.presentationPaused |
|
}); |
|
|
|
// do not replace with peerId because it can be null |
|
// if(!processUpdate) this.doNotDispatchParticipantUpdate = getPeerId(participant.peer); |
|
this.apiUpdatesManager.processUpdateMessage(updates); |
|
// if(!processUpdate) this.doNotDispatchParticipantUpdate = undefined; |
|
} |
|
|
|
public getGroupCall(id: GroupCallId) { |
|
return this.groupCalls.get(id); |
|
} |
|
|
|
public async getGroupCallFull(id: GroupCallId, override?: boolean): Promise<GroupCall> { |
|
const call = this.getGroupCall(id); |
|
if(call && call._ !== 'inputGroupCall' && !override) { |
|
return call; |
|
} |
|
|
|
const limit = this.getCachedParticipants(id).size ? 0 : GET_PARTICIPANTS_LIMIT; |
|
return this.apiManager.invokeApiSingleProcess({ |
|
method: 'phone.getGroupCall', |
|
params: { |
|
call: this.getGroupCallInput(id), |
|
limit |
|
}, |
|
processResult: (groupCall) => { |
|
// ? maybe I should save group call after participants so I can avoid passing the 'skipCounterUpdating' flag ? |
|
this.appUsersManager.saveApiUsers(groupCall.users); |
|
this.appChatsManager.saveApiChats(groupCall.chats); |
|
this.saveApiParticipants(id, groupCall.participants, true); |
|
const call = this.saveGroupCall(groupCall.call) as GroupCall; |
|
|
|
if(limit && this.nextOffsets.get(id) === undefined) { |
|
this.nextOffsets.set(id, groupCall.participants_next_offset); |
|
} |
|
|
|
return call; |
|
} |
|
}); |
|
} |
|
|
|
public saveGroupCall(call: MyGroupCall, chatId?: ChatId) { |
|
const oldCall = this.groupCalls.get(call.id); |
|
const shouldUpdate = call._ !== 'inputGroupCall' && (!oldCall || oldCall._ !== 'groupCallDiscarded'); |
|
if(oldCall) { |
|
if(shouldUpdate) { |
|
safeReplaceObject(oldCall, call); |
|
} |
|
|
|
call = oldCall; |
|
} else { |
|
this.groupCalls.set(call.id, call); |
|
} |
|
|
|
if(shouldUpdate) { |
|
this.rootScope.dispatchEvent('group_call_update', call as any); |
|
} |
|
|
|
return call; |
|
} |
|
|
|
public async createGroupCall(chatId: ChatId, scheduleDate?: number, title?: string) { |
|
const updates = await this.apiManager.invokeApi('phone.createGroupCall', { |
|
peer: this.appPeersManager.getInputPeerById(chatId.toPeerId(true)), |
|
random_id: nextRandomUint(32), |
|
schedule_date: scheduleDate, |
|
title |
|
}); |
|
|
|
this.apiUpdatesManager.processUpdateMessage(updates); |
|
|
|
const update = (updates as Updates.updates).updates.find((update) => update._ === 'updateGroupCall') as Update.updateGroupCall; |
|
return update.call; |
|
} |
|
|
|
public getGroupCallInput(id: GroupCallId): InputGroupCall { |
|
const groupCall = this.getGroupCall(id); |
|
return { |
|
_: 'inputGroupCall', |
|
id: groupCall.id, |
|
access_hash: groupCall.access_hash |
|
}; |
|
} |
|
|
|
// public generateSelfParticipant(): GroupCallParticipant { |
|
// const mainSources = this.currentGroupCall.connections.main.sources; |
|
// const presentationSources = this.currentGroupCall.connections.presentation?.sources; |
|
// return { |
|
// _: 'groupCallParticipant', |
|
// pFlags: { |
|
// can_self_unmute: true, |
|
// self: true |
|
// }, |
|
// source: mainSources.audio.source, |
|
// video: this.generateSelfVideo(mainSources.video), |
|
// presentation: presentationSources && this.generateSelfVideo(presentationSources.video, presentationSources.audio?.source), |
|
// date: tsNow(true), |
|
// peer: this.appPeersManager.getOutputPeer(rootScope.myId) |
|
// }; |
|
// } |
|
|
|
public async getGroupCallParticipants(id: GroupCallId) { |
|
const {nextOffset, setNextOffset} = this.prepareToSavingNextOffset(id); |
|
|
|
if(nextOffset !== '') { |
|
await this.apiManager.invokeApiSingleProcess({ |
|
method: 'phone.getGroupParticipants', |
|
params: { |
|
call: this.getGroupCallInput(id), |
|
ids: [], |
|
sources: [], |
|
offset: nextOffset || '', |
|
limit: GET_PARTICIPANTS_LIMIT |
|
}, |
|
processResult: (groupCallParticipants) => { |
|
const newNextOffset = groupCallParticipants.count === groupCallParticipants.participants.length ? '' : groupCallParticipants.next_offset; |
|
|
|
this.appChatsManager.saveApiChats(groupCallParticipants.chats); |
|
this.appUsersManager.saveApiUsers(groupCallParticipants.users); |
|
this.saveApiParticipants(id, groupCallParticipants.participants); |
|
|
|
setNextOffset(newNextOffset); |
|
} |
|
}); |
|
} |
|
|
|
return { |
|
participants: this.getCachedParticipants(id), |
|
isEnd: this.nextOffsets.get(id) === '' |
|
}; |
|
} |
|
|
|
public hangUp(id: GroupCallId, discard?: boolean | number) { |
|
const groupCallInput = this.getGroupCallInput(id); |
|
let promise: Promise<Updates>; |
|
if(typeof(discard) === 'boolean' && discard) { |
|
promise = this.apiManager.invokeApi('phone.discardGroupCall', { |
|
call: groupCallInput |
|
}); |
|
} else if(typeof(discard) === 'number') { |
|
promise = this.apiManager.invokeApi('phone.leaveGroupCall', { |
|
call: groupCallInput, |
|
source: discard |
|
}); |
|
} else { |
|
promise = this.apiManager.invokeApi('phone.joinGroupCall', { |
|
call: groupCallInput, |
|
join_as: this.appPeersManager.getInputPeerSelf(), |
|
muted: true, |
|
video_stopped: true, |
|
params: { |
|
_: 'dataJSON', |
|
data: '' |
|
} |
|
}); |
|
} |
|
|
|
return promise.then((updates) => { |
|
this.apiUpdatesManager.processUpdateMessage(updates); |
|
}); |
|
} |
|
|
|
public async joinGroupCall(groupCallId: GroupCallId, params: DataJSON, options: GroupCallConnectionInstance['options']) { |
|
const groupCallInput = this.getGroupCallInput(groupCallId); |
|
let promise: Promise<Updates>; |
|
if(options.type === 'main') { |
|
const request: PhoneJoinGroupCall = { |
|
call: groupCallInput, |
|
join_as: this.appPeersManager.getInputPeerSelf(), |
|
params, |
|
muted: options.isMuted, |
|
video_stopped: !options.joinVideo |
|
}; |
|
|
|
promise = this.apiManager.invokeApi('phone.joinGroupCall', request); |
|
this.log(`[api] joinGroupCall id=${groupCallId}`, request); |
|
} else { |
|
const request: PhoneJoinGroupCallPresentation = { |
|
call: groupCallInput, |
|
params |
|
}; |
|
|
|
promise = this.apiManager.invokeApi('phone.joinGroupCallPresentation', request); |
|
this.log(`[api] joinGroupCallPresentation id=${groupCallId}`, request); |
|
} |
|
|
|
const updates = await promise; |
|
this.apiUpdatesManager.processUpdateMessage(updates); |
|
|
|
const update = (updates as Updates.updates).updates.find((update) => update._ === 'updateGroupCallConnection') as Update.updateGroupCallConnection; |
|
return update; |
|
} |
|
|
|
public leaveGroupCallPresentation(groupCallId: GroupCallId) { |
|
return this.apiManager.invokeApi('phone.leaveGroupCallPresentation', { |
|
call: this.getGroupCallInput(groupCallId) |
|
}).then((updates) => { |
|
this.apiUpdatesManager.processUpdateMessage(updates); |
|
}); |
|
} |
|
}
|
|
|