Implement incoming confcall window.

This commit is contained in:
John Preston 2025-03-31 11:44:39 +04:00
parent 7c709fddba
commit a3ba99c682
11 changed files with 327 additions and 25 deletions

View file

@ -247,7 +247,49 @@ Call::Call(
setupOutgoingVideo();
}
Call::Call(
not_null<Delegate*> delegate,
not_null<UserData*> user,
CallId conferenceId,
MsgId conferenceInviteMsgId)
: _delegate(delegate)
, _user(user)
, _api(&_user->session().mtp())
, _type(Type::Incoming)
, _state(State::WaitingIncoming)
, _discardByTimeoutTimer([=] { hangup(); })
, _playbackDeviceId(
&Core::App().mediaDevices(),
Webrtc::DeviceType::Playback,
Webrtc::DeviceIdValueWithFallback(
Core::App().settings().callPlaybackDeviceIdValue(),
Core::App().settings().playbackDeviceIdValue()))
, _captureDeviceId(
&Core::App().mediaDevices(),
Webrtc::DeviceType::Capture,
Webrtc::DeviceIdValueWithFallback(
Core::App().settings().callCaptureDeviceIdValue(),
Core::App().settings().captureDeviceIdValue()))
, _cameraDeviceId(
&Core::App().mediaDevices(),
Webrtc::DeviceType::Camera,
Core::App().settings().cameraDeviceIdValue())
, _id(base::RandomValue<CallId>())
, _conferenceId(conferenceId)
, _conferenceInviteMsgId(conferenceInviteMsgId)
, _videoIncoming(
std::make_unique<Webrtc::VideoTrack>(
StartVideoState(false)))
, _videoOutgoing(
std::make_unique<Webrtc::VideoTrack>(
StartVideoState(false))) {
startWaitingTrack();
setupOutgoingVideo();
}
void Call::generateModExpFirst(bytes::const_span randomSeed) {
Expects(!conferenceInvite());
auto first = MTP::CreateModExp(_dhConfig.g, _dhConfig.p, randomSeed);
if (first.modexp.empty()) {
LOG(("Call Error: Could not compute mod-exp first."));
@ -273,6 +315,8 @@ bool Call::isIncomingWaiting() const {
}
void Call::start(bytes::const_span random) {
Expects(!conferenceInvite());
// Save config here, because it is possible that it changes between
// different usages inside the same call.
_dhConfig = _delegate->getDhConfig();
@ -297,6 +341,7 @@ void Call::startOutgoing() {
Expects(_type == Type::Outgoing);
Expects(_state.current() == State::Requesting);
Expects(_gaHash.size() == kSha256Size);
Expects(!conferenceInvite());
const auto flags = _videoCapture
? MTPphone_RequestCall::Flag::f_video
@ -350,6 +395,7 @@ void Call::startOutgoing() {
void Call::startIncoming() {
Expects(_type == Type::Incoming);
Expects(_state.current() == State::Starting);
Expects(!conferenceInvite());
_api.request(MTPphone_ReceivedCall(
MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash))
@ -363,6 +409,8 @@ void Call::startIncoming() {
}
void Call::applyUserConfirmation() {
Expects(!conferenceInvite());
if (_state.current() == State::WaitingUserConfirmation) {
setState(State::Requesting);
}
@ -375,9 +423,44 @@ void Call::answer() {
}), video);
}
void Call::acceptConferenceInvite() {
Expects(conferenceInvite());
if (_state.current() != State::WaitingIncoming) {
return;
}
setState(State::ExchangingKeys);
const auto limit = 5;
const auto messageId = _conferenceInviteMsgId;
const auto session = &_user->session();
session->api().request(MTPphone_GetGroupCall(
MTP_inputGroupCallInviteMessage(MTP_int(messageId.bare)),
MTP_int(limit)
)).done([session, messageId](const MTPphone_GroupCall &result) {
result.data().vcall().match([&](const auto &data) {
const auto call = session->data().sharedConferenceCall(
data.vid().v,
data.vaccess_hash().v);
call->processFullCall(result);
Core::App().calls().startOrJoinConferenceCall({
.call = call,
.joinMessageId = messageId,
.migrating = true,
});
});
}).fail(crl::guard(this, [=](const MTP::Error &error) {
handleRequestError(error.type());
})).send();
}
void Call::actuallyAnswer() {
Expects(_type == Type::Incoming);
if (conferenceInvite()) {
acceptConferenceInvite();
return;
}
const auto state = _state.current();
if (state != State::Starting && state != State::WaitingIncoming) {
if (state != State::ExchangingKeys
@ -435,6 +518,8 @@ void Call::setMuted(bool mute) {
}
void Call::setupMediaDevices() {
Expects(!conferenceInvite());
_playbackDeviceId.changes() | rpl::filter([=] {
return _instance && _setDeviceIdCallback;
}) | rpl::start_with_next([=](const Webrtc::DeviceResolvedId &deviceId) {
@ -530,7 +615,8 @@ crl::time Call::getDurationMs() const {
void Call::hangup(Data::GroupCall *migrateCall, const QString &migrateSlug) {
const auto state = _state.current();
if (state == State::Busy || state == State::MigrationHangingUp) {
if (state == State::Busy
|| state == State::MigrationHangingUp) {
_delegate->callFinished(this);
} else {
const auto missed = (state == State::Ringing
@ -549,6 +635,8 @@ void Call::hangup(Data::GroupCall *migrateCall, const QString &migrateSlug) {
}
void Call::redial() {
Expects(!conferenceInvite());
if (_state.current() != State::Busy) {
return;
}
@ -578,6 +666,8 @@ void Call::startWaitingTrack() {
}
void Call::sendSignalingData(const QByteArray &data) {
Expects(!conferenceInvite());
_api.request(MTPphone_SendSignalingData(
MTP_inputPhoneCall(
MTP_long(_id),
@ -776,6 +866,8 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
}
void Call::finishByMigration(const QString &slug) {
Expects(!conferenceInvite());
if (_state.current() == State::MigrationHangingUp) {
return;
}
@ -841,6 +933,7 @@ bool Call::handleSignalingData(
void Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) {
Expects(_type == Type::Outgoing);
Expects(!conferenceInvite());
if (_state.current() == State::ExchangingKeys
|| _instance) {
@ -893,6 +986,7 @@ void Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) {
void Call::startConfirmedCall(const MTPDphoneCall &call) {
Expects(_type == Type::Incoming);
Expects(!conferenceInvite());
const auto firstBytes = bytes::make_span(call.vg_a_or_b().v);
if (_gaHash != openssl::Sha256(firstBytes)) {
@ -919,6 +1013,8 @@ void Call::startConfirmedCall(const MTPDphoneCall &call) {
}
void Call::createAndStartController(const MTPDphoneCall &call) {
Expects(!conferenceInvite());
_discardByTimeoutTimer.cancel();
if (!checkCallFields(call) || _authKey.size() != kAuthKeySize) {
return;
@ -1116,6 +1212,8 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
}
void Call::handleControllerStateChange(tgcalls::State state) {
Expects(!conferenceInvite());
switch (state) {
case tgcalls::State::WaitInit: {
DEBUG_LOG(("Call Info: State changed to WaitingInit."));
@ -1390,8 +1488,11 @@ void Call::finish(
|| state == State::Ended
|| state == State::Failed) {
return;
}
if (!_id) {
} else if (conferenceInvite()) {
Core::App().calls().declineIncomingConferenceInvites(_conferenceId);
setState(finalState);
return;
} else if (!_id) {
setState(finalState);
return;
}
@ -1460,7 +1561,7 @@ void Call::handleRequestError(const QString &error) {
? Lang::Hard::CallErrorIncompatible().replace(
"{user}",
_user->name())
: QString();
: error;
if (!inform.isEmpty()) {
if (const auto window = Core::App().windowFor(
Window::SeparateId(_user))) {

View file

@ -102,6 +102,11 @@ public:
not_null<UserData*> user,
Type type,
bool video);
Call(
not_null<Delegate*> delegate,
not_null<UserData*> user,
CallId conferenceId,
MsgId conferenceInviteMsgId);
[[nodiscard]] Type type() const {
return _type;
@ -112,6 +117,15 @@ public:
[[nodiscard]] CallId id() const {
return _id;
}
[[nodiscard]] bool conferenceInvite() const {
return _conferenceId != 0;
}
[[nodiscard]] CallId conferenceId() const {
return _conferenceId;
}
[[nodiscard]] MsgId conferenceInviteMsgId() const {
return _conferenceInviteMsgId;
}
[[nodiscard]] bool isIncomingWaiting() const;
void start(bytes::const_span random);
@ -272,6 +286,7 @@ private:
bool checkCallFields(const MTPDphoneCallAccepted &call);
void actuallyAnswer();
void acceptConferenceInvite();
void confirmAcceptedCall(const MTPDphoneCallAccepted &call);
void startConfirmedCall(const MTPDphoneCall &call);
void setState(State state);
@ -325,6 +340,9 @@ private:
uint64 _accessHash = 0;
uint64 _keyFingerprint = 0;
CallId _conferenceId = 0;
MsgId _conferenceInviteMsgId = 0;
std::unique_ptr<tgcalls::Instance> _instance;
std::shared_ptr<tgcalls::VideoCaptureInterface> _videoCapture;
QString _videoCaptureDeviceId;

View file

@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/group/calls_choose_join_as.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_rtmp.h"
#include "history/history.h"
#include "history/history_item.h"
#include "mtproto/mtproto_dh_utils.h"
#include "core/application.h"
#include "core/core_settings.h"
@ -26,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/calls_panel.h"
#include "data/data_user.h"
#include "data/data_group_call.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_session.h"
@ -103,6 +106,8 @@ void Instance::Delegate::callFailed(not_null<Call*> call) {
}
void Instance::Delegate::callRedial(not_null<Call*> call) {
Expects(!call->conferenceInvite());
if (_instance->_currentCall.get() == call) {
_instance->refreshDhConfig();
}
@ -256,7 +261,7 @@ void Instance::startOrJoinConferenceCall(StartConferenceCallArgs args) {
_currentGroupCallChanges.fire_copy(raw);
if (!args.invite.empty()) {
_currentGroupCallPanel->migrationInviteUsers(std::move(args.invite));
} else if (args.migrating) {
} else if (args.migrating && !args.linkSlug.isEmpty()) {
_currentGroupCallPanel->migrationShowShareLink();
}
}
@ -442,6 +447,7 @@ void Instance::createGroupCall(
void Instance::refreshDhConfig() {
Expects(_currentCall != nullptr);
Expects(!_currentCall->conferenceInvite());
const auto weak = base::make_weak(_currentCall);
_currentCall->user()->session().api().request(MTPmessages_GetDhConfig(
@ -899,4 +905,129 @@ std::shared_ptr<tgcalls::VideoCaptureInterface> Instance::getVideoCapture(
return result;
}
const ConferenceInvites &Instance::conferenceInvites(
CallId conferenceId) const {
static const auto kEmpty = ConferenceInvites();
const auto i = _conferenceInvites.find(conferenceId);
return (i != end(_conferenceInvites)) ? i->second : kEmpty;
}
void Instance::registerConferenceInvite(
CallId conferenceId,
not_null<UserData*> user,
MsgId messageId,
bool incoming) {
auto &info = _conferenceInvites[conferenceId].users[user];
(incoming ? info.incoming : info.outgoing).emplace(messageId);
}
void Instance::unregisterConferenceInvite(
CallId conferenceId,
not_null<UserData*> user,
MsgId messageId,
bool incoming) {
const auto i = _conferenceInvites.find(conferenceId);
if (i == end(_conferenceInvites)) {
return;
}
const auto j = i->second.users.find(user);
if (j == end(i->second.users)) {
return;
}
auto &info = j->second;
(incoming ? info.incoming : info.outgoing).remove(messageId);
if (!incoming) {
user->owner().unregisterInvitedToCallUser(conferenceId, user);
}
if (info.incoming.empty() && info.outgoing.empty()) {
i->second.users.erase(j);
if (i->second.users.empty()) {
_conferenceInvites.erase(i);
}
}
if (_currentCall
&& _currentCall->user() == user
&& _currentCall->conferenceInviteMsgId() == messageId
&& _currentCall->state() == Call::State::WaitingIncoming) {
destroyCurrentCall();
}
}
void Instance::declineIncomingConferenceInvites(CallId conferenceId) {
const auto i = _conferenceInvites.find(conferenceId);
if (i == end(_conferenceInvites)) {
return;
}
for (auto j = begin(i->second.users); j != end(i->second.users);) {
const auto api = &j->first->session().api();
for (const auto &messageId : base::take(j->second.incoming)) {
api->request(MTPphone_DeclineConferenceCallInvite(
MTP_int(messageId.bare)
)).send();
}
if (j->second.outgoing.empty()) {
j = i->second.users.erase(j);
} else {
++j;
}
}
if (i->second.users.empty()) {
_conferenceInvites.erase(i);
}
}
void Instance::showConferenceInvite(
not_null<UserData*> user,
MsgId conferenceInviteMsgId) {
const auto item = user->owner().message(user, conferenceInviteMsgId);
const auto media = item ? item->media() : nullptr;
const auto call = media ? media->call() : nullptr;
const auto conferenceId = call ? call->conferenceId : 0;
if (!conferenceId
|| call->state != Data::CallState::Invitation
|| user->isSelf()) {
return;
} else if (_currentCall
&& _currentCall->conferenceId() == conferenceId) {
return;
} else if (inGroupCall()
&& _currentGroupCall->conference()
&& _currentGroupCall->conferenceCall()->id() == conferenceId) {
return;
}
const auto &config = user->session().serverConfig();
if (inCall() || inGroupCall()) {
declineIncomingConferenceInvites(conferenceId);
} else if (item->date() + (config.callRingTimeoutMs / 1000)
< base::unixtime::now()) {
declineIncomingConferenceInvites(conferenceId);
LOG(("Ignoring too old conference call invitation."));
} else {
const auto delegate = _delegate.get();
auto call = std::make_unique<Call>(
delegate,
user,
conferenceId,
conferenceInviteMsgId);
const auto raw = call.get();
user->session().account().sessionChanges(
) | rpl::start_with_next([=] {
destroyCall(raw);
}, raw->lifetime());
if (_currentCall) {
_currentCallPanel->replaceCall(raw);
std::swap(_currentCall, call);
call->hangup();
} else {
_currentCallPanel = std::make_unique<Panel>(raw);
_currentCall = std::move(call);
}
_currentCallChanges.fire_copy(raw);
}
}
} // namespace Calls

View file

@ -77,6 +77,15 @@ struct StartConferenceCallArgs {
bool migrating = false;
};
struct ConferenceInviteMessages {
base::flat_set<MsgId> incoming;
base::flat_set<MsgId> outgoing;
};
struct ConferenceInvites {
base::flat_map<not_null<UserData*>, ConferenceInviteMessages> users;
};
class Instance final : public base::has_weak_ptr {
public:
Instance();
@ -122,6 +131,23 @@ public:
-> std::shared_ptr<tgcalls::VideoCaptureInterface>;
void requestPermissionsOrFail(Fn<void()> onSuccess, bool video = true);
[[nodiscard]] const ConferenceInvites &conferenceInvites(
CallId conferenceId) const;
void registerConferenceInvite(
CallId conferenceId,
not_null<UserData*> user,
MsgId messageId,
bool incoming);
void unregisterConferenceInvite(
CallId conferenceId,
not_null<UserData*> user,
MsgId messageId,
bool incoming);
void showConferenceInvite(
not_null<UserData*> user,
MsgId conferenceInviteMsgId);
void declineIncomingConferenceInvites(CallId conferenceId);
[[nodiscard]] FnMut<void()> addAsyncWaiter();
[[nodiscard]] bool isSharingScreen() const;
@ -188,6 +214,8 @@ private:
const std::unique_ptr<Group::ChooseJoinAsProcess> _chooseJoinAs;
const std::unique_ptr<Group::StartRtmpProcess> _startWithRtmp;
base::flat_map<CallId, ConferenceInvites> _conferenceInvites;
base::flat_set<std::unique_ptr<crl::semaphore>> _asyncWaiters;
};

View file

@ -513,7 +513,14 @@ void Members::Controller::setupInvitedUsers() {
) | rpl::filter([=](const Invite &invite) {
return (invite.id == _call->id());
}) | rpl::start_with_next([=](const Invite &invite) {
if (auto row = createInvitedRow(invite.user)) {
if (invite.removed) {
if (const auto row = findRow(invite.user)) {
if (row->state() == Row::State::Invited) {
delegate()->peerListRemoveRow(row);
delegate()->peerListRefreshRows();
}
}
} else if (auto row = createInvitedRow(invite.user)) {
delegate()->peerListAppendRow(std::move(row));
delegate()->peerListRefreshRows();
}

View file

@ -789,7 +789,8 @@ void GroupCall::applyParticipantsSlice(
.videoJoined = videoJoined,
.applyVolumeFromMin = applyVolumeFromMin,
};
if (i == end(_participants)) {
const auto adding = (i == end(_participants));
if (adding) {
if (value.ssrc) {
_participantPeerByAudioSsrc.emplace(
value.ssrc,
@ -802,9 +803,6 @@ void GroupCall::applyParticipantsSlice(
participantPeer);
}
_participants.push_back(value);
if (const auto user = participantPeer->asUser()) {
_peer->owner().unregisterInvitedToCallUser(_id, user);
}
} else {
if (i->ssrc != value.ssrc) {
_participantPeerByAudioSsrc.erase(i->ssrc);
@ -836,6 +834,11 @@ void GroupCall::applyParticipantsSlice(
.now = value,
});
}
if (adding) {
if (const auto user = participantPeer->asUser()) {
_peer->owner().unregisterInvitedToCallUser(_id, user);
}
}
});
}
if (sliceSource == ApplySliceSource::UpdateReceived) {

View file

@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "calls/calls_instance.h"
#include "core/application.h"
#include "core/click_handler_types.h" // ClickHandlerContext
#include "lang/lang_keys.h"
@ -1684,11 +1685,32 @@ std::unique_ptr<HistoryView::Media> MediaLocation::createView(
MediaCall::MediaCall(not_null<HistoryItem*> parent, const Call &call)
: Media(parent)
, _call(call) {
parent->history()->owner().registerCallItem(parent);
const auto peer = parent->history()->peer;
peer->owner().registerCallItem(parent);
if (const auto user = _call.conferenceId ? peer->asUser() : nullptr) {
if (_call.state == CallState::Invitation) {
Core::App().calls().registerConferenceInvite(
_call.conferenceId,
user,
parent->id,
!parent->out());
}
}
}
MediaCall::~MediaCall() {
parent()->history()->owner().unregisterCallItem(parent());
const auto parent = this->parent();
const auto peer = parent->history()->peer;
peer->owner().unregisterCallItem(parent);
if (const auto user = _call.conferenceId ? peer->asUser() : nullptr) {
if (_call.state == CallState::Invitation) {
Core::App().calls().unregisterConferenceInvite(
_call.conferenceId,
user,
parent->id,
!parent->out());
}
}
}
std::unique_ptr<Media> MediaCall::clone(not_null<HistoryItem*> parent) {

View file

@ -1257,6 +1257,7 @@ void Session::unregisterInvitedToCallUser(
i->second.remove(user);
if (i->second.empty()) {
_invitedToCallUsers.erase(i);
_invitesToCalls.fire({ callId, user, true });
}
}
}

View file

@ -256,6 +256,7 @@ public:
struct InviteToCall {
CallId id = 0;
not_null<UserData*> user;
bool removed = false;
};
[[nodiscard]] rpl::producer<InviteToCall> invitesToCalls() const {
return _invitesToCalls.events();

View file

@ -52,7 +52,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mainwindow.h"
#include "main/main_session.h"
#include "window/notifications_manager.h"
#include "window/window_session_controller.h"
#include "calls/calls_instance.h"
#include "spellcheck/spellcheck_types.h"
#include "storage/localstorage.h"
@ -1228,8 +1227,8 @@ void History::applyServiceChanges(
}
}, [&](const MTPDmessageActionConferenceCall &data) {
if (!data.is_active() && !data.is_missed() && !item->out()) {
if (const auto window = session().tryResolveWindow()) {
window->resolveConferenceCall(item->id);
if (const auto user = item->history()->peer->asUser()) {
Core::App().calls().showConferenceInvite(user, item->id);
}
}
}, [](const auto &) {

View file

@ -881,26 +881,17 @@ void SessionNavigation::resolveConferenceCall(
data.vid().v,
data.vaccess_hash().v);
call->processFullCall(result);
const auto confirmed = std::make_shared<bool>();
const auto join = [=] {
*confirmed = true;
Core::App().calls().startOrJoinConferenceCall({
.call = call,
.linkSlug = slug,
.joinMessageId = inviteMsgId,
});
};
const auto box = uiShow()->show(Box(
uiShow()->show(Box(
Calls::Group::ConferenceCallJoinConfirm,
call,
join));
box->boxClosing() | rpl::start_with_next([=] {
if (inviteMsgId && !*confirmed) {
_api.request(MTPphone_DeclineConferenceCallInvite(
MTP_int(inviteMsgId.bare)
)).send();
}
}, box->lifetime());
if (finished) {
finished(true);
}