/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/url_auth_box.h" #include "apiwrap.h" #include "boxes/url_auth_box_content.h" #include "chat_helpers/stickers_emoji_pack.h" #include "core/application.h" #include "core/click_handler_types.h" #include "data/data_peer.h" #include "data/data_session.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_user.h" #include "history/history_item_components.h" #include "history/history_item.h" #include "history/history.h" #include "info/profile/info_profile_values.h" #include "lang/lang_keys.h" #include "main/main_account.h" #include "main/main_domain.h" #include "main/main_session.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" #include "ui/dynamic_thumbnails.h" #include "ui/emoji_config.h" #include "ui/layers/generic_box.h" #include "ui/toast/toast.h" #include "ui/widgets/menu/menu_action.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/shadow.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_settings.h" #include "styles/style_premium.h" #include "styles/style_window.h" namespace UrlAuthBox { namespace { using AnotherSessionFactory = Fn()>; using OnUserChangedCallback = Fn)>; struct SwitchAccountResult { Ui::RpWidget *widget = nullptr; AnotherSessionFactory anotherSession; OnUserChangedCallback setOnUserChanged; Fn updateUserIdHint; }; [[nodiscard]] std::shared_ptr MakeMatchCodeImage( not_null session, const QString &code) { auto emojiLength = 0; const auto emoji = Ui::Emoji::Find(code, &emojiLength); if (!emoji || emojiLength != code.size()) { return nullptr; } const auto makeFor = [&](not_null source) { if (const auto sticker = source->emojiStickersPack() .stickerForEmoji(emoji)) { return Ui::MakeEmojiThumbnail( &source->data(), Data::SerializeCustomEmojiId(sticker.document), nullptr, nullptr, 1); } return std::shared_ptr(); }; if (session->isTestMode()) { for (const auto &account : Core::App().domain().orderedAccounts()) { if (!account->sessionExists() || account->session().isTestMode()) { continue; } if (const auto image = makeFor(&account->session())) { return image; } } } return makeFor(session); } [[nodiscard]] SwitchAccountResult AddAccountsMenu( not_null parent, UserId userIdHint = UserId()) { const auto session = &Core::App().domain().active().session(); const auto widget = Ui::CreateChild( parent, st::restoreUserpicIcon.photoSize + st::lineWidth * 8); struct State { base::unique_qptr menu; UserData *currentUser = nullptr; Fn onUserChanged; }; const auto state = widget->lifetime().make_state(); const auto isCurrentTest = session->isTestMode(); const auto findHintedUser = [&]() -> UserData* { if (!userIdHint) { return session->user().get(); } for (const auto &account : Core::App().domain().orderedAccounts()) { if (!account->sessionExists() || (account->session().isTestMode() != isCurrentTest)) { continue; } if (account->session().userId() == userIdHint) { return account->session().user(); } } return session->user().get(); }; state->currentUser = findHintedUser(); const auto userpic = Ui::CreateChild( parent, state->currentUser, st::restoreUserpicIcon); widget->setUserpic(userpic); const auto filtered = [=] { auto result = std::vector>(); for (const auto &account : Core::App().domain().orderedAccounts()) { if (!account->sessionExists() || (account->session().user() == state->currentUser) || (account->session().isTestMode() != isCurrentTest)) { continue; } result.push_back(&account->session()); } return result; }; const auto isSingle = filtered().empty(); widget->setExpanded(!isSingle); widget->setAttribute(Qt::WA_TransparentForMouseEvents, isSingle); widget->setClickedCallback([=] { const auto &st = st::popupMenuWithIcons; state->menu = base::make_unique_q(widget, st); for (const auto &anotherSession : filtered()) { const auto user = anotherSession->user(); const auto action = new QAction(user->name(), state->menu); QObject::connect(action, &QAction::triggered, [=] { state->currentUser = user; const auto newUserpic = Ui::CreateChild( parent, user, st::restoreUserpicIcon); widget->setUserpic(newUserpic); if (state->onUserChanged) { state->onUserChanged(); } }); auto owned = base::make_unique_q( state->menu->menu(), state->menu->menu()->st(), action, nullptr, nullptr); const auto menuUserpic = Ui::CreateChild( owned.get(), user, st::lockSetupEmailUserpicSmall); menuUserpic->setAttribute(Qt::WA_TransparentForMouseEvents); menuUserpic->move(st.menu.itemIconPosition); state->menu->addAction(std::move(owned)); } state->menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); state->menu->popup( widget->mapToGlobal( QPoint( widget->width() + Ui::BoxShadow::ExtendFor(st.shadow).right(), widget->height()))); }); return { widget, [=] { return &state->currentUser->session(); }, [=](Fn callback) { state->onUserChanged = callback; }, [=](UserId newUserIdHint) { const auto isCurrentTest = session->isTestMode(); for (const auto &acc : Core::App().domain().orderedAccounts()) { if (!acc->sessionExists() || (acc->session().isTestMode() != isCurrentTest)) { continue; } if (acc->session().userId() == newUserIdHint) { state->currentUser = acc->session().user(); const auto next = Ui::CreateChild( parent, state->currentUser, st::restoreUserpicIcon); widget->setUserpic(next); break; } } }, }; } } // namespace void RequestButton( std::shared_ptr show, const MTPDurlAuthResultRequest &request, not_null message, int row, int column); void RequestUrl( std::shared_ptr show, const MTPDurlAuthResultRequest &request, not_null session, const QString &url, QVariant context); void ActivateButton( std::shared_ptr show, not_null message, int row, int column) { const auto itemId = message->fullId(); const auto button = HistoryMessageMarkupButton::Get( &message->history()->owner(), itemId, row, column); if (button->requestId || !message->isRegular()) { return; } const auto session = &message->history()->session(); const auto inputPeer = message->history()->peer->input(); const auto buttonId = button->buttonId; const auto url = QString::fromUtf8(button->data); using Flag = MTPmessages_RequestUrlAuth::Flag; button->requestId = session->api().request(MTPmessages_RequestUrlAuth( MTP_flags(Flag::f_peer | Flag::f_msg_id | Flag::f_button_id), inputPeer, MTP_int(itemId.msg), MTP_int(buttonId), MTPstring(), // #TODO auth url MTPstring() // in_app_origin )).done([=](const MTPUrlAuthResult &result) { const auto button = HistoryMessageMarkupButton::Get( &session->data(), itemId, row, column); if (!button) { return; } button->requestId = 0; result.match([&](const MTPDurlAuthResultAccepted &data) { if (const auto url = data.vurl()) { UrlClickHandler::Open(qs(url->v)); } }, [&](const MTPDurlAuthResultDefault &data) { HiddenUrlClickHandler::Open(url); }, [&](const MTPDurlAuthResultRequest &data) { if (const auto item = session->data().message(itemId)) { RequestButton(show, data, item, row, column); } }); }).fail([=] { const auto button = HistoryMessageMarkupButton::Get( &session->data(), itemId, row, column); if (!button) { return; } button->requestId = 0; HiddenUrlClickHandler::Open(url); }).send(); } void ActivateUrl( std::shared_ptr show, not_null session, const QString &url, QVariant context) { context = QVariant::fromValue([&] { auto result = context.value(); result.skipBotAutoLogin = true; return result; }()); using Flag = MTPmessages_RequestUrlAuth::Flag; session->api().request(MTPmessages_RequestUrlAuth( MTP_flags(Flag::f_url), MTPInputPeer(), MTPint(), // msg_id MTPint(), // button_id MTP_string(url), MTPstring() // in_app_origin )).done([=](const MTPUrlAuthResult &result) { result.match([&](const MTPDurlAuthResultAccepted &data) { UrlClickHandler::Open(qs(data.vurl().value_or_empty()), context); }, [&](const MTPDurlAuthResultDefault &data) { HiddenUrlClickHandler::Open(url, context); }, [&](const MTPDurlAuthResultRequest &data) { RequestUrl(show, data, session, url, context); }); }).fail([=](const MTP::Error &error) { if (error.type() == u"URL_EXPIRED"_q) { show->showToast( tr::lng_url_auth_phone_toast_bad_expired(tr::now)); } else { HiddenUrlClickHandler::Open(url, context); } }).send(); } void RequestButton( std::shared_ptr show, const MTPDurlAuthResultRequest &request, not_null message, int row, int column) { const auto itemId = message->fullId(); const auto button = HistoryMessageMarkupButton::Get( &message->history()->owner(), itemId, row, column); if (!button || button->requestId || !message->isRegular()) { return; } const auto session = &message->history()->session(); const auto inputPeer = message->history()->peer->input(); const auto buttonId = button->buttonId; const auto url = QString::fromUtf8(button->data); const auto bot = request.is_request_write_access() ? session->data().processUser(request.vbot()).get() : nullptr; const auto box = std::make_shared>(); const auto finishWithUrl = [=](const QString &url, bool accepted) { if (*box) { (*box)->closeBox(); } if (url.isEmpty() && accepted) { show->showToast(tr::lng_passport_success(tr::now)); } else { UrlClickHandler::Open(url); } }; const auto callback = [=](Result result) { if (!result.auth) { session->api().request(MTPmessages_DeclineUrlAuth( MTP_string(url) )).send(); finishWithUrl(url, false); } else if (session->data().message(itemId)) { using Flag = MTPmessages_AcceptUrlAuth::Flag; const auto flags = Flag(0) | (result.allowWrite ? Flag::f_write_allowed : Flag(0)) | (result.sharePhone ? Flag::f_share_phone_number : Flag(0)) | (result.matchCode.isEmpty() ? Flag(0) : Flag::f_match_code) | (Flag::f_peer | Flag::f_msg_id | Flag::f_button_id); session->api().request(MTPmessages_AcceptUrlAuth( MTP_flags(flags), inputPeer, MTP_int(itemId.msg), MTP_int(buttonId), MTPstring(), result.matchCode.isEmpty() ? MTPstring() : MTP_string(result.matchCode) )).done([=](const MTPUrlAuthResult &result) { const auto accepted = result.match( [](const MTPDurlAuthResultAccepted &data) { return true; }, [](const auto &) { return false; }); const auto to = result.match( [&](const MTPDurlAuthResultAccepted &data) { return qs(data.vurl().value_or_empty()); }, [&](const MTPDurlAuthResultDefault &data) { return url; }, [&](const MTPDurlAuthResultRequest &data) { LOG(("API Error: " "got urlAuthResultRequest after acceptUrlAuth.")); return url; }); finishWithUrl(to, accepted); }).fail([=] { finishWithUrl(url, false); }).send(); } }; const auto displayName = request.is_is_app() ? (request.vverified_app_name() ? qs(*request.vverified_app_name()) : tr::lng_url_auth_unverified_app(tr::now)) : qs(request.vdomain()); *box = show->show( Box( Show, url, displayName, session->user()->name(), bot->firstName, callback), Ui::LayerOption::KeepOther); } void RequestUrl( std::shared_ptr show, const MTPDurlAuthResultRequest &request, not_null session, const QString &url, QVariant context) { struct State { base::weak_qptr box; AnotherSessionFactory anotherSession = nullptr; QString firstMatchCode; rpl::lifetime boxDeclineLifetime; rpl::lifetime matchCodesBoxDeclineLifetime; }; const auto bot = request.is_request_write_access() ? session->data().processUser(request.vbot()).get() : nullptr; const auto requestPhone = request.is_request_phone_number(); const auto matchCodesFirst = request.is_match_codes_first(); const auto isApp = request.is_is_app(); const auto domain = isApp ? (request.vverified_app_name() ? qs(*request.vverified_app_name()) : tr::lng_url_auth_unverified_app(tr::now)) : qs(request.vdomain()); const auto userIdHint = request.vuser_id_hint() ? peerToUser(peerFromUser(*request.vuser_id_hint())) : UserId(); const auto matchCodes = [&] { auto result = QStringList(); if (const auto codes = request.vmatch_codes()) { for (const auto &code : codes->v) { result.push_back(qs(code)); } } return result; }(); const auto state = std::make_shared(); const auto finishWithUrl = [=](const QString &to, bool accepted) { if (state->box) { state->box->closeBox(); } if ((to.isEmpty() && accepted) || (to == url)) { } else { UrlClickHandler::Open(to, context); } }; const auto resolveSession = [=] { return state->anotherSession ? state->anotherSession() : session; }; const auto requestDecline = [=] { resolveSession()->api().request(MTPmessages_DeclineUrlAuth( MTP_string(url) )).send(); }; const auto sendRequest = [=](Result result) { if (!result.auth) { requestDecline(); finishWithUrl(url, false); } else { const auto sharePhone = result.sharePhone; using Flag = MTPmessages_AcceptUrlAuth::Flag; const auto flags = Flag::f_url | (result.allowWrite ? Flag::f_write_allowed : Flag(0)) | (sharePhone ? Flag::f_share_phone_number : Flag(0)) | (result.matchCode.isEmpty() ? Flag(0) : Flag::f_match_code); resolveSession()->api().request(MTPmessages_AcceptUrlAuth( MTP_flags(flags), MTPInputPeer(), MTPint(), // msg_id MTPint(), // button_id MTP_string(url), result.matchCode.isEmpty() ? MTPstring() : MTP_string(result.matchCode) )).done([=](const MTPUrlAuthResult &result) { const auto accepted = result.match( [](const MTPDurlAuthResultAccepted &data) { return true; }, [](const auto &) { return false; }); const auto to = result.match( [&](const MTPDurlAuthResultAccepted &data) { return qs(data.vurl().value_or_empty()); }, [&](const MTPDurlAuthResultDefault &data) { return url; }, [&](const MTPDurlAuthResultRequest &data) { LOG(("API Error: " "got urlAuthResultRequest after acceptUrlAuth.")); return url; }); finishWithUrl(to, accepted); const auto domainWrapped = isApp ? tr::bold(domain) : tr::link(domain); show->showToast(Ui::Toast::Config{ .title = tr::lng_url_auth_phone_toast_good_title(tr::now), .text = ((requestPhone && !sharePhone) ? tr::lng_url_auth_phone_toast_good_no_phone : tr::lng_url_auth_phone_toast_good)( tr::now, lt_domain, domainWrapped, tr::marked), .duration = crl::time(4000), }); }).fail([=] { const auto domainWrapped = isApp ? tr::bold(domain) : tr::link(domain); show->showToast(Ui::Toast::Config{ .title = tr::lng_url_auth_phone_toast_bad_title(tr::now), .text = tr::lng_url_auth_phone_toast_bad( tr::now, lt_domain, domainWrapped, tr::marked), .duration = crl::time(4000), }); finishWithUrl(url, false); }).send(); } }; const auto browser = qs(request.vbrowser().value_or("Unknown browser")); const auto device = qs(request.vplatform().value_or("Unknown platform")); const auto ip = qs(request.vip().value_or("Unknown IP")); const auto region = qs(request.vregion().value_or("Unknown region")); const auto showAuthBox = [=] { state->box = show->show(Box([=](not_null box) { const auto accountResult = box->lifetime().make_state< SwitchAccountResult>(nullptr); const auto matchCodesShared = box->lifetime().make_state< rpl::variable>(matchCodes); const auto reloadRequest = [=] { using Flag = MTPmessages_RequestUrlAuth::Flag; const auto currentSession = resolveSession(); currentSession->api().request(MTPmessages_RequestUrlAuth( MTP_flags(Flag::f_url), MTPInputPeer(), MTPint(), // msg_id MTPint(), // button_id MTP_string(url), MTPstring() // in_app_origin )).done(crl::guard(box, [=](const MTPUrlAuthResult &result) { result.match([&](const MTPDurlAuthResultRequest &data) { const auto newUserId = data.vuser_id_hint() ? peerToUser(peerFromUser(*data.vuser_id_hint())) : UserId(); accountResult->updateUserIdHint(newUserId); auto newCodes = QStringList(); if (const auto codes = data.vmatch_codes()) { for (const auto &code : codes->v) { newCodes.push_back(qs(code)); } } *matchCodesShared = newCodes; }, [](const auto &) {}); })).send(); }; const auto callback = [=](Result result) { state->boxDeclineLifetime.destroy(); if (result.matchCode.isEmpty() && !state->firstMatchCode.isEmpty()) { result.matchCode = state->firstMatchCode; } if (!requestPhone) { return sendRequest(result); } box->uiShow()->show(Box([=](not_null box) { box->setTitle(tr::lng_url_auth_phone_sure_title()); const auto confirm = [=](bool confirmed) { return [=](Fn close) { auto copy = result; copy.sharePhone = confirmed; sendRequest(copy); close(); }; }; const auto currentSession = resolveSession(); const auto capitalized = [=](const QString &v) { return v.left(1).toUpper() + v.mid(1).toLower(); }; using namespace Info::Profile; Ui::ConfirmBox( box, Ui::ConfirmBoxArgs{ .text = tr::lng_url_auth_phone_sure_text( lt_domain, rpl::single( tr::bold(isApp ? domain : capitalized(domain))), lt_phone, PhoneValue(currentSession->user()), tr::rich), .confirmed = confirm(true), .cancelled = confirm(false), .confirmText = tr::lng_allow_bot(), .cancelText = tr::lng_url_auth_phone_sure_deny(), }); })); }; ShowDetails( box, url, domain, [=](QString code) -> std::shared_ptr { return MakeMatchCodeImage(resolveSession(), code); }, callback, (bot ? object_ptr( box->verticalLayout(), bot, st::defaultUserpicButton, Ui::PeerUserpicShape::Forum) : nullptr), bot ? Info::Profile::NameValue(bot) : nullptr, browser, device, ip, region, (matchCodesFirst ? (rpl::single(QStringList()) | rpl::type_erased) : matchCodesShared->value()), isApp); *accountResult = AddAccountsMenu( box->verticalLayout(), userIdHint); box->verticalLayout()->widthValue( ) | rpl::on_next([=, w = (*accountResult).widget] { w->moveToRight(st::lineWidth * 4, 0); }, (*accountResult).widget->lifetime()); state->anotherSession = (*accountResult).anotherSession; (*accountResult).setOnUserChanged(reloadRequest); })); state->box->boxClosing() | rpl::on_next([=] { requestDecline(); }, state->boxDeclineLifetime); }; if (!matchCodesFirst || matchCodes.isEmpty()) { showAuthBox(); return; } auto matchCodesBox = base::weak_qptr(); matchCodesBox = show->show( Box([=](not_null matchBox) { ShowMatchCodesBox( matchBox, [=](QString code) -> std::shared_ptr { return MakeMatchCodeImage(resolveSession(), code); }, domain, matchCodes, [=](QString matchCode) { state->matchCodesBoxDeclineLifetime.destroy(); resolveSession()->api().request( MTPmessages_CheckUrlAuthMatchCode( MTP_string(url), MTP_string(matchCode)) ).done([=](const MTPBool &result) { if (!mtpIsTrue(result)) { show->showToast( tr::lng_url_auth_phone_toast_bad_expired( tr::now)); return; } state->firstMatchCode = std::move(matchCode); showAuthBox(); }).fail([=](const MTP::Error &error) { show->showToast((error.type() == u"URL_EXPIRED"_q) ? tr::lng_url_auth_phone_toast_bad_expired( tr::now) : error.type()); }).send(); }, isApp); }), Ui::LayerOption::KeepOther); matchCodesBox->boxClosing() | rpl::on_next([=] { requestDecline(); }, state->matchCodesBoxDeclineLifetime); } } // namespace UrlAuthBox