diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 6651b2477c..273b999533 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -225,6 +225,8 @@ PRIVATE boxes/peers/channel_ownership_transfer.h boxes/peers/choose_peer_box.cpp boxes/peers/choose_peer_box.h + boxes/peers/create_managed_bot_box.cpp + boxes/peers/create_managed_bot_box.h boxes/peers/edit_contact_box.cpp boxes/peers/edit_contact_box.h boxes/peers/edit_forum_topic_box.cpp diff --git a/Telegram/Resources/icons/chat/code_tags.svg b/Telegram/Resources/icons/chat/code_tags.svg new file mode 100644 index 0000000000..f03e9ce1b3 --- /dev/null +++ b/Telegram/Resources/icons/chat/code_tags.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c959d4b667..4bfbb5e3b7 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2546,6 +2546,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_new_creator_pending" = "{user} will become the new main admin in 7 days if {from} does not return."; "lng_action_managed_bot_created" = "{from} created a bot {bot}."; +"lng_create_bot_title" = "Create Bot"; +"lng_create_bot_subtitle" = "{bot} would like to create and manage a chatbot on your behalf."; +"lng_create_bot_name_placeholder" = "Bot Name"; +"lng_create_bot_username_placeholder" = "Bot Username"; +"lng_create_bot_username_available" = "{username} is available."; +"lng_create_bot_username_taken" = "This username is already taken."; +"lng_create_bot_username_bad_symbols" = "Username can only contain a-z, 0-9, and underscores."; +"lng_create_bot_username_too_short" = "Username must be at least 5 characters."; +"lng_create_bot_button" = "Create"; +"lng_managed_bot_label" = "{icon} Created and managed by {bot}."; +"lng_managed_bot_ready" = "**{name}** is ready!\n\nClick **Start** below to test your new chatbot. Its behavior is defined by **{parent}**."; + "lng_stake_game_title" = "Emoji Stake"; "lng_stake_game_beta" = "Beta"; "lng_stake_game_about" = "A limited play-test of the upcoming emoji mini-game platform for a small group of users."; diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index fc02a07dbe..ea764b8e71 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -11,10 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_cloud_password.h" #include "api/api_send_progress.h" #include "api/api_suggest_post.h" -#include "boxes/share_box.h" -#include "boxes/passcode_box.h" -#include "boxes/url_auth_box.h" #include "boxes/peers/choose_peer_box.h" +#include "boxes/peers/create_managed_bot_box.h" +#include "boxes/passcode_box.h" +#include "boxes/share_box.h" +#include "boxes/url_auth_box.h" #include "lang/lang_keys.h" #include "chat_helpers/bot_command.h" #include "core/core_cloud_password.h" @@ -40,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" +#include #include #include @@ -546,6 +548,46 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { QVariant::fromValue(context), }); } break; + + case ButtonType::CreateBot: { + HideSingleUseKeyboard(controller, item); + + auto suggestedName = QString(); + auto suggestedUsername = QString(); + { + auto stream = QDataStream(button->data); + stream >> suggestedName >> suggestedUsername; + } + const auto peer = item->history()->peer; + const auto itemId = item->id; + const auto id = int32(button->buttonId); + const auto bot = item->getMessageBot(); + if (!bot) { + break; + } + ShowCreateManagedBotBox({ + .show = controller->uiShow(), + .manager = bot, + .suggestedName = suggestedName, + .suggestedUsername = suggestedUsername, + .done = [=](not_null createdBot) { + using Flag = MTPmessages_SendBotRequestedPeer::Flag; + peer->session().api().request( + MTPmessages_SendBotRequestedPeer( + MTP_flags(Flag::f_msg_id), + peer->input(), + MTP_int(itemId), + MTPstring(), + MTP_int(id), + MTP_vector( + 1, + createdBot->input())) + ).done([=](const MTPUpdates &result) { + peer->session().api().applyUpdates(result); + }).send(); + }, + }); + } break; } } diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index bbb0ab721a..de28a78580 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2111,6 +2111,18 @@ void Updates::feedUpdate(const MTPUpdate &update) { } } break; + case mtpc_updateManagedBot: { + const auto &d = update.c_updateManagedBot(); + if (const auto user = session().data().userLoaded( + UserId(d.vuser_id().v))) { + session().api().requestFullPeer(user); + } + if (const auto bot = session().data().userLoaded( + UserId(d.vbot_id().v))) { + session().api().requestFullPeer(bot); + } + } break; + case mtpc_updatePendingJoinRequests: { const auto &d = update.c_updatePendingJoinRequests(); if (const auto peer = session().data().peerLoaded(peerFromMTP(d.vpeer()))) { diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 6ca5eda121..68d777ce9e 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1235,3 +1235,29 @@ disableSharingButtonLock: IconEmoji { icon: icon {{ "emoji/premium_lock", activeButtonFg }}; padding: margins(-2px, 1px, 0px, 0px); } + +createBotBox: Box(defaultBox) { + shadowIgnoreTopSkip: true; +} +createBotUserpicPadding: margins(0px, 16px, 0px, 10px); +createBotTitlePadding: margins(0px, 0px, 0px, 8px); +createBotSubtitlePadding: margins(0px, 0px, 0px, 4px); +createBotCenteredText: FlatLabel(defaultFlatLabel) { + align: align(top); + minWidth: 40px; +} +createBotFieldSpacing: 8px; +createBotUsernameField: InputField(defaultInputField) { + textMargins: margins(0px, 28px, 0px, 4px); +} +createBotUsernamePrefix: FlatLabel(defaultFlatLabel) { + style: boxTextStyle; +} +createBotUsernameSuffix: FlatLabel(createBotUsernamePrefix) { + textFg: windowSubTextFg; +} + +managedBotIconEmoji: IconEmoji { + icon: icon {{ "chat/code_tags", windowSubTextFg }}; + padding: margins(-2px, -2px, -2px, 0px); +} diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp index 908579b221..3d55b4aa12 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/add_contact_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/premium_limits_box.h" +#include "chat_helpers/compose/compose_show.h" #include "data/data_chat.h" #include "data/data_channel.h" #include "data/data_peer.h" @@ -37,7 +38,7 @@ class ChoosePeerBoxController final , public base::has_weak_ptr { public: ChoosePeerBoxController( - not_null navigation, + not_null session, not_null bot, RequestPeerQuery query, Fn>)> callback); @@ -59,7 +60,7 @@ private: void prepareRestrictions(); - const not_null _navigation; + const not_null _session; not_null _bot; RequestPeerQuery _query; base::flat_set> _commonGroups; @@ -324,12 +325,12 @@ object_ptr CreatePeerByQueryBox( } ChoosePeerBoxController::ChoosePeerBoxController( - not_null navigation, + not_null session, not_null bot, RequestPeerQuery query, Fn>)> callback) -: ChatsListBoxController(&navigation->session()) -, _navigation(navigation) +: ChatsListBoxController(session) +, _session(session) , _bot(bot) , _query(query) , _callback(std::move(callback)) { @@ -339,7 +340,7 @@ ChoosePeerBoxController::ChoosePeerBoxController( } Main::Session &ChoosePeerBoxController::session() const { - return _navigation->session(); + return *_session; } void ChoosePeerBoxController::prepareRestrictions() { @@ -380,8 +381,16 @@ void ChoosePeerBoxController::prepareRestrictions() { }, icon->lifetime()); button->setClickedCallback([=] { - _navigation->parentController()->show( - CreatePeerByQueryBox(_navigation, _bot, _query, _callback)); + const auto controller = ChatHelpers::ResolveWindowDefault()( + _session); + if (controller) { + delegate()->peerListUiShow()->showBox( + CreatePeerByQueryBox( + controller, + _bot, + _query, + _callback)); + } }); button->events( @@ -495,13 +504,26 @@ void ShowChoosePeerBox( not_null bot, RequestPeerQuery query, Fn>)> chosen) { + ShowChoosePeerBox( + navigation->uiShow(), + bot, + query, + std::move(chosen)); +} + +void ShowChoosePeerBox( + std::shared_ptr show, + not_null bot, + RequestPeerQuery query, + Fn>)> chosen) { + const auto session = &show->session(); const auto needCommonGroups = query.isBotParticipant && (query.type == RequestPeerQuery::Type::Group) && !query.myRights; - if (needCommonGroups && !bot->session().api().botCommonGroups(bot)) { - const auto weak = base::make_weak(navigation); - bot->session().api().requestBotCommonGroups(bot, [=] { - if (const auto strong = weak.get()) { + if (needCommonGroups && !session->api().botCommonGroups(bot)) { + const auto weak = std::weak_ptr(show); + session->api().requestBotCommonGroups(bot, [=] { + if (const auto strong = weak.lock()) { ShowChoosePeerBox(strong, bot, query, chosen); } }); @@ -517,7 +539,7 @@ void ShowChoosePeerBox( }; const auto limit = query.maxQuantity; auto controller = std::make_unique( - navigation, + session, bot, query, std::move(callback)); @@ -540,7 +562,7 @@ void ShowChoosePeerBox( }); }, box->lifetime()); }; - *weak = navigation->parentController()->show(Box( + *weak = show->show(Box( std::move(controller), std::move(initBox))); } diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.h b/Telegram/SourceFiles/boxes/peers/choose_peer_box.h index 982ec4784c..3ba5268cef 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.h +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.h @@ -9,6 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL struct RequestPeerQuery; +namespace Main { +class Session; +class SessionShow; +} // namespace Main + namespace Ui { class BoxContent; } // namespace Ui @@ -22,3 +27,9 @@ void ShowChoosePeerBox( not_null bot, RequestPeerQuery query, Fn>)> chosen); + +void ShowChoosePeerBox( + std::shared_ptr show, + not_null bot, + RequestPeerQuery query, + Fn>)> chosen); diff --git a/Telegram/SourceFiles/boxes/peers/create_managed_bot_box.cpp b/Telegram/SourceFiles/boxes/peers/create_managed_bot_box.cpp new file mode 100644 index 0000000000..68d6cef02d --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/create_managed_bot_box.cpp @@ -0,0 +1,347 @@ +/* +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/peers/create_managed_bot_box.h" + +#include "base/timer.h" +#include "boxes/peers/edit_peer_common.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "lang/lang_keys.h" +#include "main/session/session_show.h" +#include "main/main_session.h" +#include "mtproto/sender.h" +#include "ui/controls/userpic_button.h" +#include "ui/layers/generic_box.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/widgets/fields/special_fields.h" +#include "ui/widgets/labels.h" +#include "ui/vertical_list.h" + +#include "styles/style_boxes.h" +#include "styles/style_layers.h" + +void CreateManagedBotBox( + not_null box, + CreateManagedBotDescriptor &&descriptor) { + struct State { + base::Timer checkTimer; + mtpRequestId checkRequestId = 0; + mtpRequestId createRequestId = 0; + QString checkUsername; + QString errorText; + QString goodText; + }; + const auto show = descriptor.show; + const auto session = &show->session(); + const auto viaDeeplink = descriptor.viaDeeplink; + const auto done = std::move(descriptor.done); + const auto manager = descriptor.manager; + + const auto api = box->lifetime().make_state( + &session->mtp()); + const auto state = box->lifetime().make_state(); + + box->setStyle(st::createBotBox); + box->setWidth(st::boxWidth); + box->setNoContentMargin(true); + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + + if (manager) { + auto userpic = object_ptr( + box, + manager, + st::defaultUserpicButton); + userpic->setAttribute(Qt::WA_TransparentForMouseEvents); + box->addRow( + std::move(userpic), + st::boxRowPadding + st::createBotUserpicPadding, + style::al_top); + } + + box->addRow( + object_ptr( + box, + tr::lng_create_bot_title(), + st::boxTitle), + st::boxRowPadding + st::createBotTitlePadding, + style::al_top); + + if (manager) { + box->addRow( + object_ptr( + box, + tr::lng_create_bot_subtitle( + lt_bot, + rpl::single(tr::bold(manager->name())), + tr::rich), + st::createBotCenteredText), + st::boxRowPadding + st::createBotSubtitlePadding, + style::al_top + )->setTryMakeSimilarLines(true); + } + + const auto name = box->addRow(object_ptr( + box, + st::defaultInputField, + tr::lng_create_bot_name_placeholder(), + descriptor.suggestedName)); + name->setMaxLength(Ui::EditPeer::kMaxGroupChannelTitle); + + Ui::AddSkip(box->verticalLayout(), st::createBotFieldSpacing); + + const auto botPrefixText = u"@"_q; + const auto botSuffixText = u"bot"_q; + + auto initialUsername = descriptor.suggestedUsername; + if (initialUsername.startsWith(botPrefixText)) { + initialUsername.remove(0, botPrefixText.size()); + } + if (initialUsername.endsWith(botSuffixText, Qt::CaseInsensitive)) { + initialUsername.chop(3); + } + + const auto fieldSt = box->lifetime().make_state( + st::createBotUsernameField); + fieldSt->textMargins.setLeft( + st::defaultFlatLabel.style.font->width(botPrefixText)); + fieldSt->textMargins.setRight( + st::createBotUsernameSuffix.style.font->width(botSuffixText)); + fieldSt->placeholderMargins.setLeft(-fieldSt->textMargins.left()); + fieldSt->placeholderMargins.setRight(-fieldSt->textMargins.right()); + const auto usernameWrap = box->addRow(object_ptr(box)); + const auto username = Ui::CreateChild( + usernameWrap, + *fieldSt, + tr::lng_create_bot_username_placeholder(), + initialUsername, + QString()); // linkPlaceholder + usernameWrap->widthValue() | rpl::on_next([=](int width) { + username->resizeToWidth(width); + }, username->lifetime()); + username->heightValue() | rpl::on_next([=](int height) { + usernameWrap->resize(usernameWrap->width(), height); + }, username->lifetime()); + + const auto botPrefix = Ui::CreateChild( + username, + botPrefixText, + st::createBotUsernamePrefix); + const auto botSuffix = Ui::CreateChild( + username, + botSuffixText, + st::createBotUsernameSuffix); + botPrefix->setAttribute(Qt::WA_TransparentForMouseEvents); + botPrefix->show(); + botSuffix->setAttribute(Qt::WA_TransparentForMouseEvents); + botSuffix->show(); + + const auto updatePositions = [=] { + const auto &margin = fieldSt->textMargins; + const auto &font = fieldSt->style.font; + const auto text = username->getLastText(); + const auto textWidth = font->width(text); + const auto maxX = username->width() - margin.right(); + const auto x = std::min(margin.left() + textWidth, maxX); + botPrefix->move(0, margin.top()); + botSuffix->move(x, margin.top()); + }; + + username->geometryValue( + ) | rpl::on_next(updatePositions, username->lifetime()); + + const auto statusWrapper = box->addRow( + object_ptr(box), + st::boxRowPadding + QMargins(0, st::defaultVerticalListSkip, 0, 0)); + + const auto statusLabel = Ui::CreateChild( + statusWrapper, + st::aboutRevokePublicLabel); + statusLabel->move(0, 0); + + const auto maxHeight = box->lifetime().make_state(0); + statusLabel->heightValue( + ) | rpl::on_next([=](int height) { + const auto newMax = std::max({ + *maxHeight, + height, + st::aboutRevokePublicLabel.style.font->height, + }); + if (*maxHeight != newMax) { + *maxHeight = newMax; + statusWrapper->resize(statusWrapper->width(), newMax); + } + }, statusWrapper->lifetime()); + + statusWrapper->widthValue( + ) | rpl::on_next([=](int width) { + statusLabel->resizeToWidth(width); + }, statusWrapper->lifetime()); + + box->setFocusCallback([=] { name->setFocusFast(); }); + + const auto setError = [=](const QString &text) { + state->errorText = text; + state->goodText = QString(); + statusLabel->setText(text); + statusLabel->setTextColorOverride( + text.isEmpty() + ? std::optional() + : st::boxTextFgError->c); + state->checkTimer.cancel(); + }; + + const auto checkUsername = [=] { + api->request(base::take(state->checkRequestId)).cancel(); + + auto raw = username->getLastText().trimmed(); + if (raw.startsWith(QChar('@'))) { + raw = raw.mid(1); + } + const auto value = raw + u"bot"_q; + if (value.size() < Ui::EditPeer::kMinUsernameLength) { + return; + } + state->checkUsername = value; + state->checkRequestId = api->request(MTPbots_CheckUsername( + MTP_string(value) + )).done([=](const MTPBool &result) { + state->checkRequestId = 0; + if (mtpIsTrue(result)) { + state->errorText = QString(); + state->goodText = tr::lng_create_bot_username_available( + tr::now, + lt_username, + state->checkUsername); + statusLabel->setText(state->goodText); + statusLabel->setTextColorOverride(st::boxTextFgGood->c); + } else { + setError(tr::lng_create_bot_username_taken(tr::now)); + } + }).fail([=](const MTP::Error &error) { + state->checkRequestId = 0; + setError(tr::lng_create_bot_username_taken(tr::now)); + }).send(); + }; + + state->checkTimer.setCallback(checkUsername); + + const auto usernameChanged = [=] { + const auto value = username->getLastText().trimmed(); + if (value.isEmpty()) { + setError(QString()); + return; + } + + const auto len = int(value.size()); + for (auto i = 0; i < len; ++i) { + const auto ch = value.at(i); + if ((ch < 'A' || ch > 'Z') + && (ch < 'a' || ch > 'z') + && (ch < '0' || ch > '9') + && ch != '_') { + setError( + tr::lng_create_bot_username_bad_symbols(tr::now)); + return; + } + } + + if ((value + u"bot"_q).size() < Ui::EditPeer::kMinUsernameLength) { + setError(tr::lng_create_bot_username_too_short(tr::now)); + return; + } + + setError(QString()); + state->checkTimer.callOnce(Ui::EditPeer::kUsernameCheckTimeout); + }; + + const auto submit = [=] { + if (state->createRequestId) { + return; + } + + const auto nameValue = name->getLastText().trimmed(); + if (nameValue.isEmpty()) { + name->showError(); + return; + } + + auto rawUsername = username->getLastText().trimmed(); + if (rawUsername.startsWith(QChar('@'))) { + rawUsername = rawUsername.mid(1); + } + const auto usernameValue = rawUsername + u"bot"_q; + if (rawUsername.isEmpty()) { + username->showError(); + return; + } + + if (!state->errorText.isEmpty() || state->goodText.isEmpty()) { + username->showError(); + return; + } + + using Flag = MTPbots_CreateBot::Flag; + const auto flags = viaDeeplink ? Flag::f_via_deeplink : Flag(0); + const auto managerInput = manager + ? manager->inputUser() + : MTP_inputUserEmpty(); + + const auto weak = base::make_weak(box); + state->createRequestId = api->request(MTPbots_CreateBot( + MTP_flags(flags), + MTP_string(nameValue), + MTP_string(usernameValue), + managerInput + )).done([=](const MTPUser &result) { + state->createRequestId = 0; + const auto user = session->data().processUser(result); + if (done) { + done(user); + } + if (const auto strong = weak.get()) { + strong->closeBox(); + } + }).fail([=](const MTP::Error &error) { + state->createRequestId = 0; + const auto type = error.type(); + if (type == u"USERNAME_OCCUPIED"_q + || type == u"USERNAME_INVALID"_q) { + username->showError(); + setError(tr::lng_create_bot_username_taken(tr::now)); + } else { + show->showToast(type); + } + }).send(); + }; + + QObject::connect(username, &Ui::UsernameInput::changed, [=] { + usernameChanged(); + updatePositions(); + }); + + name->submits( + ) | rpl::on_next([=](auto) { + username->setFocus(); + }, name->lifetime()); + + QObject::connect(username, &Ui::UsernameInput::submitted, submit); + + box->addButton(tr::lng_create_bot_button(), submit); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + + if (!descriptor.suggestedUsername.isEmpty()) { + usernameChanged(); + } +} + +void ShowCreateManagedBotBox(CreateManagedBotDescriptor &&descriptor) { + const auto show = descriptor.show; + show->showBox(Box(CreateManagedBotBox, std::move(descriptor))); +} diff --git a/Telegram/SourceFiles/boxes/peers/create_managed_bot_box.h b/Telegram/SourceFiles/boxes/peers/create_managed_bot_box.h new file mode 100644 index 0000000000..d7bb08f334 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/create_managed_bot_box.h @@ -0,0 +1,33 @@ +/* +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 +*/ +#pragma once + +class UserData; + +namespace Main { +class SessionShow; +} // namespace Main + +namespace Ui { +class GenericBox; +} // namespace Ui + +struct CreateManagedBotDescriptor { + std::shared_ptr show; + UserData *manager = nullptr; + QString suggestedName; + QString suggestedUsername; + bool viaDeeplink = false; + Fn)> done; +}; + +void CreateManagedBotBox( + not_null box, + CreateManagedBotDescriptor &&descriptor); + +void ShowCreateManagedBotBox(CreateManagedBotDescriptor &&descriptor); diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_new.cpp b/Telegram/SourceFiles/core/deep_links/deep_links_new.cpp index cbcd6037ff..3659cac18e 100644 --- a/Telegram/SourceFiles/core/deep_links/deep_links_new.cpp +++ b/Telegram/SourceFiles/core/deep_links/deep_links_new.cpp @@ -7,6 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "core/deep_links/deep_links_router.h" +#include "apiwrap.h" +#include "boxes/peers/create_managed_bot_box.h" +#include "data/data_peer_id.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "main/main_session.h" #include "window/window_session_controller.h" namespace Core::DeepLinks { @@ -36,6 +42,54 @@ Result ShowAddContact(const Context &ctx) { return Result::Handled; } +Result ShowNewBot(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + const auto manager = ctx.params.value(u"manager"_q); + const auto username = ctx.params.value(u"username"_q); + const auto title = ctx.params.value(u"name"_q); + if (manager.isEmpty()) { + return Result::Handled; + } + const auto session = &ctx.controller->session(); + const auto weak = base::make_weak(ctx.controller); + session->api().request(MTPcontacts_ResolveUsername( + MTP_flags(0), + MTP_string(manager), + MTP_string() + )).done([=](const MTPcontacts_ResolvedPeer &result) { + const auto strong = weak.get(); + if (!strong) { + return; + } + result.match([&](const MTPDcontacts_resolvedPeer &data) { + strong->session().data().processUsers(data.vusers()); + strong->session().data().processChats(data.vchats()); + const auto peerId = peerFromMTP(data.vpeer()); + if (const auto managerBot = strong->session().data().userLoaded( + peerToUser(peerId))) { + if (!managerBot->isBot()) { + return; + } + ShowCreateManagedBotBox({ + .show = strong->uiShow(), + .manager = managerBot, + .suggestedName = title, + .suggestedUsername = username, + .viaDeeplink = true, + .done = [weak](not_null createdBot) { + if (const auto strong = weak.get()) { + strong->showPeerHistory(createdBot); + } + }, + }); + } + }); + }).send(); + return Result::Handled; +} + } // namespace void RegisterNewHandlers(Router &router) { @@ -53,6 +107,11 @@ void RegisterNewHandlers(Router &router) { .path = u"contact"_q, .action = CodeBlock{ ShowAddContact }, }); + + router.add(u"newbot"_q, { + .path = QString(), + .action = CodeBlock{ ShowNewBot }, + }); } } // namespace Core::DeepLinks diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index c35238cfbe..fa1e04bfe0 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -1887,6 +1887,18 @@ QString TryConvertUrlToLocal(QString url) { } else if (const auto callMatch = regex_match(u"^call/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) { const auto slug = callMatch->captured(1); return u"tg://call?slug="_q + slug; + } else if (const auto newbotMatch = regex_match(u"^newbot/([a-zA-Z0-9\\.\\_]+)(/([a-zA-Z0-9\\.\\_]+))?(\\?(.+))?$"_q, query, matchOptions)) { + const auto manager = newbotMatch->captured(1); + const auto username = newbotMatch->captured(3); + const auto params = newbotMatch->captured(5); + auto result = u"tg://newbot?manager="_q + url_encode(manager); + if (!username.isEmpty()) { + result += u"&username="_q + url_encode(username); + } + if (!params.isEmpty()) { + result += '&' + params; + } + return result; } else if (const auto privateMatch = regex_match(u"^" "c/(\\-?\\d+)" "(" diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 86c23814be..0300436a0e 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -318,6 +318,14 @@ void UserData::setPersonalChannel(ChannelId channelId, MsgId messageId) { } } +UserId UserData::botManagerId() const { + return _botManagerId; +} + +void UserData::setBotManagerId(UserId managerId) { + _botManagerId = managerId; +} + MTPInputUser UserData::inputUser() const { const auto item = isLoaded() ? nullptr : owner().messageWithPeer(id); if (item) { @@ -993,6 +1001,7 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user->setPersonalChannel( update.vpersonal_channel_id().value_or_empty(), update.vpersonal_channel_message().value_or_empty()); + user->setBotManagerId(update.vbot_manager_id().value_or_empty()); if (user->isSelf()) { user->owner().businessInfo().applyAwaySettings( FromMTP(&user->owner(), update.vbusiness_away_message())); diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index e969efc4b6..bde10b135b 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -291,6 +291,9 @@ public: [[nodiscard]] MsgId personalChannelMessageId() const; void setPersonalChannel(ChannelId channelId, MsgId messageId); + [[nodiscard]] UserId botManagerId() const; + void setBotManagerId(UserId managerId); + [[nodiscard]] MTPInputUser inputUser() const; QString firstName; @@ -334,6 +337,7 @@ private: ChannelId _personalChannelId = 0; MsgId _personalChannelMessageId = 0; + UserId _botManagerId = 0; uint64 _accessHash = 0; static constexpr auto kInaccessibleAccessHashOld diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.cpp b/Telegram/SourceFiles/history/history_item_reply_markup.cpp index d2aa2f7b27..92c75259b3 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.cpp +++ b/Telegram/SourceFiles/history/history_item_reply_markup.cpp @@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_components.h" #include "inline_bots/bot_attach_web_view.h" +#include + namespace { [[nodiscard]] HistoryMessageMarkupButton::Visual ParseVisual( @@ -36,7 +38,9 @@ namespace { }; } -[[nodiscard]] RequestPeerQuery RequestPeerQueryFromTL( +} // namespace + +RequestPeerQuery RequestPeerQueryFromTL( const MTPDkeyboardButtonRequestPeer &query) { using Type = RequestPeerQuery::Type; using Restriction = RequestPeerQuery::Restriction; @@ -75,8 +79,6 @@ namespace { return result; } -} // namespace - InlineBots::PeerTypes PeerTypesFromMTP( const MTPvector &types) { using namespace InlineBots; @@ -170,16 +172,38 @@ void HistoryMessageMarkupData::fillRows( qs(data.vtext()), ParseVisual(data.vstyle())); }, [&](const MTPDkeyboardButtonRequestPeer &data) { - const auto query = RequestPeerQueryFromTL(data); - row.emplace_back( - Type::RequestPeer, - qs(data.vtext()), - ParseVisual(data.vstyle()), - QByteArray( - reinterpret_cast(&query), - sizeof(query)), - QString(), - int64(data.vbutton_id().v)); + data.vpeer_type().match([&]( + const MTPDrequestPeerTypeCreateBot &createData) { + auto serialized = QByteArray(); + { + auto stream = QDataStream( + &serialized, + QIODevice::WriteOnly); + stream + << qs(createData.vsuggested_name() + .value_or_empty()) + << qs(createData.vsuggested_username() + .value_or_empty()); + } + row.emplace_back( + Type::CreateBot, + qs(data.vtext()), + ParseVisual(data.vstyle()), + serialized, + QString(), + int64(data.vbutton_id().v)); + }, [&](const auto &) { + const auto query = RequestPeerQueryFromTL(data); + row.emplace_back( + Type::RequestPeer, + qs(data.vtext()), + ParseVisual(data.vstyle()), + QByteArray( + reinterpret_cast(&query), + sizeof(query)), + QString(), + int64(data.vbutton_id().v)); + }); }, [&](const MTPDkeyboardButtonUrl &data) { row.emplace_back( Type::Url, diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h index 2614b20c87..8df3597074 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.h +++ b/Telegram/SourceFiles/history/history_item_reply_markup.h @@ -69,6 +69,11 @@ struct RequestPeerQuery { }; static_assert(std::is_trivially_copy_assignable_v); +class MTPDkeyboardButtonRequestPeer; + +[[nodiscard]] RequestPeerQuery RequestPeerQueryFromTL( + const MTPDkeyboardButtonRequestPeer &query); + struct HistoryMessageMarkupButton { enum class Type : uchar { Default, @@ -92,6 +97,7 @@ struct HistoryMessageMarkupButton { SuggestDecline, SuggestAccept, SuggestChange, + CreateBot, }; enum class Color : uchar { diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index 544be0cace..3bd6f439a3 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -7,10 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_about_view.h" +#include "api/api_peer_colors.h" #include "api/api_premium.h" #include "api/api_sending.h" #include "apiwrap.h" #include "base/random.h" +#include "base/unixtime.h" #include "ui/effects/premium_stars.h" #include "boxes/premium_preview_box.h" #include "chat_helpers/stickers_lottie.h" @@ -21,6 +23,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" #include "data/data_document.h" +#include "data/data_emoji_statuses.h" +#include "data/data_photo.h" #include "data/data_session.h" #include "data/data_user.h" #include "history/view/history_view_group_call_bar.h" @@ -40,15 +44,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/sections/settings_credits.h" // BuyStarsHandler #include "settings/sections/settings_premium.h" #include "ui/chat/chat_style.h" +#include "ui/image/image_location_factory.h" #include "ui/text/custom_emoji_instance.h" #include "ui/text/text_utilities.h" #include "ui/text/text_options.h" #include "ui/dynamic_image.h" +#include "ui/empty_userpic.h" #include "ui/painter.h" +#include "ui/top_background_gradient.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" // GroupCallUserpics #include "styles/style_credits.h" +#include "styles/style_menu_icons.h" namespace HistoryView { namespace { @@ -603,6 +611,83 @@ bool EmptyChatLockedBox::hasHeavyPart() { void EmptyChatLockedBox::unloadHeavyPart() { } +QImage GenerateManagedBotImage(not_null user) { + auto centerColor = QColor(); + auto edgeColor = QColor(); + if (const auto collectible = user->emojiStatusId().collectible) { + centerColor = collectible->centerColor; + edgeColor = collectible->edgeColor; + } else if (const auto color + = user->session().api().peerColors().colorProfileFor(user)) { + if (color->bg.size() > 1) { + centerColor = color->bg[1]; + edgeColor = color->bg[0]; + } + } + if (!centerColor.isValid()) { + const auto colorIndex = Ui::EmptyUserpic::ColorIndex( + user->id.value); + const auto colors = Ui::EmptyUserpic::UserpicColor(colorIndex); + centerColor = colors.color1->c; + edgeColor = colors.color2->c; + } + + const auto size = QSize( + st::managedBotImageWidth, + st::managedBotImageHeight); + auto image = Ui::CreateTopBgGradient( + size, + centerColor, + edgeColor, + false); + if (image.isNull()) { + return image; + } + + auto p = QPainter(&image); + auto hq = PainterHighQualityEnabler(p); + + auto iconColor = edgeColor.toHsv(); + iconColor.setHsv( + iconColor.hsvHue(), + iconColor.hsvSaturation(), + std::max(iconColor.value() - 64, 0)); + iconColor = iconColor.toRgb(); + const auto width = size.width(); + const auto height = size.height(); + const auto &icon = st::menuIconBot; + const auto &points = Ui::PatternBgPoints(); + for (const auto &point : points) { + const auto cx = point.position.x() * width; + const auto cy = point.position.y() * height; + p.save(); + p.setOpacity(point.opacity); + if (point.scale < 1.) { + p.translate(cx, cy); + p.scale(point.scale, point.scale); + p.translate(-cx, -cy); + } + const auto x = int(cx) - icon.width() / 2; + const auto y = int(cy) - icon.height() / 2; + icon.paint(p, x, y, width, iconColor); + p.restore(); + } + + const auto ratio = style::DevicePixelRatio(); + const auto iheight = st::managedBotCodeIcon.height(); + const auto scale = (size.height() * ratio * 100) / iheight; + auto iconImage = st::managedBotCodeIcon.instance(Qt::white, scale, true); + iconImage.setDevicePixelRatio(ratio); + const auto iw = iconImage.width() / ratio; + const auto ih = iconImage.height() / ratio; + p.drawImage( + QRect((width - iw) / 2, (height - ih) / 2, iw, ih), + iconImage); + + p.end(); + return image; +} + } // namespace AboutView::AboutView( @@ -704,6 +789,12 @@ bool AboutView::refresh() { } setItem(makeNewBotThread(), nullptr); return true; + } else if (user->botManagerId() && info->description.isEmpty()) { + if (_item) { + return false; + } + setItem(makeManagedBotInfo(user), nullptr); + return true; } const auto version = info->descriptionVersion; if (_version == version) { @@ -1062,4 +1153,41 @@ AdminLog::OwnedItem AboutView::makeNewBotThread() { return result; } +AdminLog::OwnedItem AboutView::makeManagedBotInfo( + not_null user) { + const auto image = GenerateManagedBotImage(user); + const auto photoImage = image.isNull() + ? ImageWithLocation() + : Images::FromImageInMemory(image, "PNG"); + const auto photo = _history->session().data().photo( + base::RandomValue(), + uint64(0), + QByteArray(), + base::unixtime::now(), + 0, + false, + QByteArray(), + ImageWithLocation{}, + photoImage, + photoImage, + ImageWithLocation{}, + ImageWithLocation{}, + crl::time(0)); + + const auto managerId = user->botManagerId(); + const auto managerUser = user->owner().userLoaded(managerId); + const auto parentName = managerUser + ? managerUser->name() + : QString(); + const auto text = tr::lng_managed_bot_ready( + tr::now, + lt_name, + tr::bold(user->name()), + lt_parent, + tr::bold(parentName), + tr::rich); + + return makeAboutSimple(text, nullptr, photo); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.h b/Telegram/SourceFiles/history/view/history_view_about_view.h index e9ddfb2fae..e9fed1829a 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.h +++ b/Telegram/SourceFiles/history/view/history_view_about_view.h @@ -53,6 +53,8 @@ private: not_null user); [[nodiscard]] AdminLog::OwnedItem makeBlocked(); [[nodiscard]] AdminLog::OwnedItem makeNewBotThread(); + [[nodiscard]] AdminLog::OwnedItem makeManagedBotInfo( + not_null user); void makeIntro(not_null user); void setItem(AdminLog::OwnedItem item, DocumentData *sticker); void setHelloChosen(not_null sticker); diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index f3d69fc137..62f63e4306 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -1165,7 +1165,8 @@ Element::Element( if (user && user->isBot() && !user->isRepliesChat() - && !user->isVerifyCodes()) { + && !user->isVerifyCodes() + && !user->botManagerId()) { AddComponents(FakeBotAboutTop::Bit()); } } diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index 43c71b0e2b..6d4eb7dc9d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -208,12 +208,15 @@ QSize Photo::countOptimalSize() { auto maxWidth = qMax(maxActualWidth, scaled.height()); auto minHeight = qMax(scaled.height(), st::minPhotoSize); if (_parent->hasBubble()) { + const auto botTop = _parent->Get(); const auto captionMaxWidth = _parent->textualMaxWidth(); - const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth); - maxWidth = qMin(qMax(maxWidth, maxWithCaption), st::msgMaxWidth); - minHeight = adjustHeightForLessCrop( - dimensions, - { maxWidth, minHeight }); + if (botTop || !_parent->data()->isFakeAboutView()) { + const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth); + maxWidth = qMin(qMax(maxWidth, maxWithCaption), st::msgMaxWidth); + minHeight = adjustHeightForLessCrop( + dimensions, + { maxWidth, minHeight }); + } } return { maxWidth, minHeight }; } @@ -246,11 +249,15 @@ QSize Photo::countCurrentSize(int newWidth) { if (botTop) { accumulate_max(captionMaxWidth, botTop->maxWidth); } - const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth); - newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth); - newHeight = adjustHeightForLessCrop( - dimensions, - { newWidth, newHeight }); + if (botTop || !_parent->data()->isFakeAboutView()) { + const auto maxWithCaption = qMin( + st::msgMaxWidth, + captionMaxWidth); + newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth); + newHeight = adjustHeightForLessCrop( + dimensions, + { newWidth, newHeight }); + } } if (newWidth >= maxWidth()) { newHeight = qMin(newHeight, minHeight()); @@ -529,8 +536,8 @@ QImage Photo::prepareImageCacheWithLarge(QSize outer, Image *large) const { blurred = thumbnail; } else if (const auto small = _dataMedia->image(Size::Small)) { blurred = small; - } else { - blurred = large; + } else { + blurred = large; } const auto resize = large ? ::Media::Streaming::DecideFrameResize(outer, large->size()) @@ -539,9 +546,9 @@ QImage Photo::prepareImageCacheWithLarge(QSize outer, Image *large) const { } void Photo::paintUserpicFrame( - Painter &p, - QPoint photoPosition, - bool markFrameShown) const { + Painter &p, + QPoint photoPosition, + bool markFrameShown) const { const auto autoplay = _data->videoCanBePlayed() && videoAutoplayEnabled(); const auto startPlay = autoplay && !_streamed; @@ -596,9 +603,9 @@ void Photo::paintUserpicFrame( } void Photo::paintUserpicFrame( - Painter &p, - const PaintContext &context, - QPoint photoPosition) const { + Painter &p, + const PaintContext &context, + QPoint photoPosition) const { paintUserpicFrame(p, photoPosition, !context.paused); if (_data->videoCanBePlayed() && !_streamed) { @@ -628,6 +635,9 @@ void Photo::paintUserpicFrame( QSize Photo::photoSize() const { if (_storyId) { return { kStoryWidth, kStoryHeight }; + } else if (_parent->data()->isFakeAboutView() + && !_parent->Get()) { + return { st::managedBotImageWidth, st::managedBotImageHeight }; } return QSize(_data->width(), _data->height()); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 55c9dac807..ebd134c918 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -79,6 +79,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rect.h" #include "ui/ui_utility.h" #include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" #include "ui/text/text_variant.h" #include "ui/toast/toast.h" #include "ui/vertical_list.h" @@ -95,6 +96,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_controller.h" // Window::Controller::show. #include "window/window_peer_menu.h" #include "window/window_session_controller.h" +#include "styles/style_boxes.h" #include "styles/style_channel_earn.h" // st::channelEarnCurrencyCommonMargins #include "styles/style_info.h" #include "styles/style_layers.h" @@ -1218,7 +1220,7 @@ public: private: object_ptr setupPersonalChannel(not_null user); object_ptr setupInfo(); - void setupMainApp(); + void setupMainApp(bool suppressBottom = false); void setupBotPermissions(); void addShowTopicsListButton( Ui::MultiSlideTracker &tracker, @@ -2124,7 +2126,7 @@ object_ptr DetailsFiller::setupPersonalChannel( return result; } -void DetailsFiller::setupMainApp() { +void DetailsFiller::setupMainApp(bool suppressBottom) { const auto button = _wrap->add( object_ptr( _wrap, @@ -2148,6 +2150,9 @@ void DetailsFiller::setupMainApp() { }); const auto url = tr::lng_mini_apps_tos_url(tr::now); + const auto parts = suppressBottom + ? RectPart::Top + : (RectPart::Top | RectPart::Bottom); const auto divider = Ui::AddDividerText( _wrap, rpl::combine( @@ -2163,7 +2168,10 @@ void DetailsFiller::setupMainApp() { text = text.append(u"\n\n"_q).append(verify->description); } return text; - })); + }), + st::defaultBoxDividerLabelPadding, + st::defaultDividerLabel, + parts); divider->setClickHandlerFilter([=](const auto &...) { UrlClickHandler::Open(url); return false; @@ -2370,12 +2378,48 @@ object_ptr DetailsFiller::fill() { if (const auto info = user->botInfo.get()) { if (info->hasMainApp) { _dividerOverridden.force_assign(true); - setupMainApp(); + const auto managedBotFollows = user->botManagerId() + && !info->canManageEmojiStatus + && user->owner().userLoaded(user->botManagerId()); + setupMainApp(managedBotFollows); } if (info->canManageEmojiStatus) { _dividerOverridden.force_assign(false); setupBotPermissions(); } + if (user->botManagerId()) { + if (const auto managerUser = user->owner().userLoaded( + user->botManagerId())) { + if (!info->hasMainApp) { + _dividerOverridden.force_assign(true); + } + const auto botUsername = managerUser->username(); + const auto linkText = botUsername.isEmpty() + ? managerUser->name() + : (u"@"_q + botUsername); + const auto parts = (info->hasMainApp && !info->canManageEmojiStatus) + ? RectPart::Bottom + : (RectPart::Top | RectPart::Bottom); + const auto divider = Ui::AddDividerText( + _wrap, + tr::lng_managed_bot_label( + lt_icon, + rpl::single(Ui::Text::IconEmoji(&st::managedBotIconEmoji)), + lt_bot, + rpl::single(tr::link(linkText)), + tr::marked), + st::defaultBoxDividerLabelPadding, + st::defaultDividerLabel, + parts); + const auto weak = base::make_weak(_controller); + divider->setClickHandlerFilter([=](const auto &...) { + if (const auto strong = weak.get()) { + strong->showPeerInfo(managerUser); + } + return false; + }); + } + } } if (!user->isSelf() && !_sublist) { addReportReaction(_mainTracker, &lastButtonTracker); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index aef877d07a..b4570a5f8d 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -17,6 +17,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/random.h" #include "base/timer_rpl.h" #include "base/unixtime.h" +#include "boxes/peers/choose_peer_box.h" +#include "boxes/peers/create_managed_bot_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/premium_preview_box.h" #include "boxes/share_box.h" @@ -43,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" +#include "history/history_item_reply_markup.h" #include "info/bot/starref/info_bot_starref_common.h" // MakePeerBubbleButton #include "info/profile/info_profile_values.h" #include "inline_bots/inline_bot_result.h" @@ -1975,6 +1978,75 @@ void WebViewInstance::botSendPreparedMessage( }).send(); } +void WebViewInstance::botRequestChat( + Ui::BotWebView::RequestChatRequest request) { + const auto bot = _bot; + const auto callback = request.callback; + const auto requestId = request.requestId; + if (!_panel) { + callback(u"UNKNOWN_ERROR"_q); + return; + } + const auto show = uiShow(); + bot->session().api().request(MTPbots_GetRequestedWebViewButton( + bot->inputUser(), + MTP_string(requestId) + )).done([show, bot, callback, requestId]( + const MTPKeyboardButton &result) { + result.match([&](const MTPDkeyboardButtonRequestPeer &data) { + if (!*show) { + callback(u"UNKNOWN_ERROR"_q); + return; + } + const auto buttonId = data.vbutton_id(); + const auto sendPeers = [=]( + std::vector> peers) { + using Flag = MTPmessages_SendBotRequestedPeer::Flag; + bot->session().api().request( + MTPmessages_SendBotRequestedPeer( + MTP_flags(Flag::f_webapp_req_id), + bot->input(), + MTPint(), + MTP_string(requestId), + buttonId, + MTP_vector_from_range( + peers | ranges::views::transform([]( + not_null peer) { + return MTPInputPeer(peer->input()); + }))) + ).done([=](const MTPUpdates &result) { + bot->session().api().applyUpdates(result); + callback(QString()); + }).fail([callback](const MTP::Error &error) { + callback(error.type()); + }).send(); + }; + data.vpeer_type().match([&]( + const MTPDrequestPeerTypeCreateBot &createData) { + ShowCreateManagedBotBox({ + .show = show, + .manager = bot, + .suggestedName = qs( + createData.vsuggested_name().value_or_empty()), + .suggestedUsername = qs( + createData.vsuggested_username() + .value_or_empty()), + .done = [=](not_null createdBot) { + sendPeers({ createdBot }); + }, + }); + }, [&](const auto &) { + const auto query = RequestPeerQueryFromTL(data); + ShowChoosePeerBox(show, bot, query, sendPeers); + }); + }, [&](const auto &) { + callback(u"UNSUPPORTED_BUTTON_TYPE"_q); + }); + }).fail([callback](const MTP::Error &error) { + callback(error.type()); + }).send(); +} + void WebViewInstance::botSetEmojiStatus( Ui::BotWebView::SetEmojiStatusRequest request) { const auto bot = _bot; diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 422219c221..f1c35dafd0 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -300,6 +300,8 @@ private: Ui::BotWebView::CustomMethodRequest request) override; void botSendPreparedMessage( Ui::BotWebView::SendPreparedMessageRequest request) override; + void botRequestChat( + Ui::BotWebView::RequestChatRequest request) override; void botSetEmojiStatus( Ui::BotWebView::SetEmojiStatusRequest request) override; void botDownloadFile( diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 14626742ad..70daf5c353 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -1027,6 +1027,8 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { processBottomBarColor(arguments); } else if (command == "web_app_send_prepared_message") { processSendMessageRequest(arguments); + } else if (command == "web_app_request_chat") { + processRequestChat(arguments); } else if (command == "web_app_set_emoji_status") { processEmojiStatusRequest(arguments); } else if (command == "web_app_request_emoji_status_access") { @@ -1214,6 +1216,34 @@ void Panel::processSendMessageRequest(const QJsonObject &args) { }); } +void Panel::processRequestChat(const QJsonObject &args) { + if (args.isEmpty()) { + _delegate->botClose(); + return; + } + const auto requestId = args["req_id"].toString(); + if (requestId.isEmpty()) { + return; + } + auto callback = crl::guard(this, [=](QString error) { + if (error.isEmpty()) { + postEvent( + "requested_chat_sent", + u"{ req_id: \"%1\" }"_q.arg(requestId)); + } else { + postEvent( + "requested_chat_failed", + u"{ req_id: \"%1\", error: \"%2\" }"_q.arg( + requestId, + error)); + } + }); + _delegate->botRequestChat({ + .requestId = requestId, + .callback = std::move(callback), + }); +} + void Panel::processEmojiStatusRequest(const QJsonObject &args) { if (args.isEmpty()) { _delegate->botClose(); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index d698e9f0fd..5185d80d37 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -79,6 +79,11 @@ struct SendPreparedMessageRequest { Fn callback; }; +struct RequestChatRequest { + QString requestId; + Fn callback; +}; + class Delegate { public: [[nodiscard]] virtual Webview::ThemeParams botThemeParams() = 0; @@ -111,6 +116,7 @@ public: virtual void botDownloadFile(DownloadFileRequest request) = 0; virtual void botSendPreparedMessage( SendPreparedMessageRequest request) = 0; + virtual void botRequestChat(RequestChatRequest request) = 0; virtual void botVerifyAge(int age) = 0; virtual void botOpenPrivacyPolicy() = 0; virtual void botClose() = 0; @@ -176,6 +182,7 @@ private: void sendDataMessage(const QJsonObject &args); void switchInlineQueryMessage(const QJsonObject &args); void processSendMessageRequest(const QJsonObject &args); + void processRequestChat(const QJsonObject &args); void processEmojiStatusRequest(const QJsonObject &args); void processEmojiStatusAccessRequest(); void processStorageSaveKey(const QJsonObject &args); diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index d3228885a3..ccba464478 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1551,6 +1551,10 @@ newThreadAboutIconSkip: chatIntroTitleMargin.top; newThreadAboutIconOuter: 60px; newThreadAboutIcon: icon{{ "chat/new_thread-40x40", windowFgActive }}; +managedBotCodeIcon: icon{{ "chat/code_tags", windowFg }}; +managedBotImageWidth: 240px; +managedBotImageHeight: 120px; + stakeBox: Box(defaultBox) { buttonPadding: margins(24px, 12px, 24px, 24px); buttonHeight: 42px; diff --git a/Telegram/lib_ui b/Telegram/lib_ui index a0d830f2bd..bcd1aaf92c 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit a0d830f2bd39b215545001431f4ce581d28e676b +Subproject commit bcd1aaf92c80588a6d015521eb9ce662e3731d9a