/* 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 "chat_helpers/compose/compose_show.h" #include "data/data_premium_limits.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 "settings/sections/settings_premium.h" #include "ui/controls/userpic_button.h" #include "ui/layers/generic_box.h" #include "ui/toast/toast.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; bool created = false; bool hadOccupiedError = false; }; const auto show = descriptor.show; const auto session = &show->session(); const auto viaDeeplink = descriptor.viaDeeplink; const auto done = std::move(descriptor.done); const auto cancelled = std::move(descriptor.cancelled); 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(); }); 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); 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; while (initialUsername.startsWith(botPrefixText)) { initialUsername.remove(0, botPrefixText.size()); } if (initialUsername.endsWith(botSuffixText, Qt::CaseInsensitive)) { initialUsername.chop(botSuffixText.size()); } 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()); username->setPlaceholderHidden(true); username->setMaxLength( Ui::EditPeer::kMaxUsernameLength - int(botSuffixText.size())); 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()); username->finishAnimating(); 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::createBotStatusLabel); 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::createBotStatusLabel.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 showLinkInfo = [=] { auto raw = username->getLastText().trimmed(); while (raw.startsWith(botPrefixText)) { raw = raw.mid(botPrefixText.size()); } const auto full = raw + botSuffixText; const auto text = tr::lng_create_bot_username_link( tr::now, lt_link, u"t.me/"_q + full); state->errorText = QString(); state->goodText = text; statusLabel->setText(text); statusLabel->setTextColorOverride(st::usernameDefaultFg->c); }; const auto checkUsername = [=] { api->request(base::take(state->checkRequestId)).cancel(); auto raw = username->getLastText().trimmed(); while (raw.startsWith(botPrefixText)) { raw = raw.mid(botPrefixText.size()); } const auto value = raw + botSuffixText; 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)) { if (state->hadOccupiedError) { 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 { showLinkInfo(); } } else { state->hadOccupiedError = true; setError(tr::lng_create_bot_username_taken(tr::now)); } }).fail([=](const MTP::Error &error) { state->checkRequestId = 0; state->hadOccupiedError = true; 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 + botSuffixText).size() < Ui::EditPeer::kMinUsernameLength) { setError(tr::lng_create_bot_username_too_short(tr::now)); return; } showLinkInfo(); 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(); while (rawUsername.startsWith(botPrefixText)) { rawUsername = rawUsername.mid(botPrefixText.size()); } const auto usernameValue = rawUsername + botSuffixText; 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 weak = base::make_weak(box); state->createRequestId = api->request(MTPbots_CreateBot( MTP_flags(flags), MTP_string(nameValue), MTP_string(usernameValue), manager->inputUser() )).done([=](const MTPUser &result) { state->createRequestId = 0; const auto user = session->data().processUser(result); state->created = true; 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) { state->hadOccupiedError = true; username->showError(); setError(tr::lng_create_bot_username_taken(tr::now)); } else if (type == u"BOT_CREATE_LIMIT_EXCEEDED"_q) { const auto limits = Data::PremiumLimits(session); const auto premium = session->premium(); const auto premiumPossible = session->premiumPossible(); const auto defaultLimit = limits.botsCreateDefault(); const auto premiumLimit = limits.botsCreatePremium(); const auto current = premium ? premiumLimit : defaultLimit; const auto bot = tr::link( u"@BotFather"_q, u"https://t.me/botfather?start=deletebot"_q); if (premium || !premiumPossible) { using WeakToast = base::weak_ptr; const auto toast = std::make_shared(); (*toast) = show->showToast({ .text = tr::lng_bots_create_limit_final( tr::now, lt_count, current, lt_bot, tr::bold(bot), tr::rich), .filter = crl::guard(session, [=]( const ClickHandlerPtr &, Qt::MouseButton button) { if (button == Qt::LeftButton) { if (const auto strong = toast->get()) { strong->hideAnimated(); (*toast) = nullptr; } } return true; }), }); } else { Settings::ShowPremiumPromoToast( show, ChatHelpers::ResolveWindowDefault(), tr::lng_bots_create_limit( tr::now, lt_count, current, lt_link, tr::bold( tr::lng_bots_create_limit_link(tr::now, tr::link)), lt_premium_count, tr::bold(QString::number(premiumLimit)), lt_bot, tr::bold(bot), tr::rich), u"managed_bots"_q); } } else if (MTP::IsFloodError(error)) { show->showToast(tr::lng_flood_error(tr::now)); } else { show->showToast(type); } }).handleFloodErrors().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(); } box->boxClosing() | rpl::on_next([=] { if (!state->created && cancelled) { cancelled(); } }, box->lifetime()); } void ShowCreateManagedBotBox(CreateManagedBotDescriptor &&descriptor) { const auto show = descriptor.show; show->showBox(Box(CreateManagedBotBox, std::move(descriptor))); }