bot_attach_web_view.cpp 73 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "inline_bots/bot_attach_web_view.h"
  8. #include "api/api_blocked_peers.h"
  9. #include "api/api_common.h"
  10. #include "api/api_sending.h"
  11. #include "apiwrap.h"
  12. #include "base/call_delayed.h"
  13. #include "base/base_file_utilities.h"
  14. #include "base/qthelp_url.h"
  15. #include "base/random.h"
  16. #include "base/timer_rpl.h"
  17. #include "base/unixtime.h"
  18. #include "boxes/peer_list_controllers.h"
  19. #include "boxes/premium_preview_box.h"
  20. #include "boxes/share_box.h"
  21. #include "chat_helpers/stickers_lottie.h"
  22. #include "chat_helpers/tabbed_panel.h"
  23. #include "core/application.h"
  24. #include "core/click_handler_types.h"
  25. #include "core/local_url_handlers.h"
  26. #include "core/shortcuts.h"
  27. #include "core/ui_integration.h" // TextContext
  28. #include "data/components/location_pickers.h"
  29. #include "data/data_bot_app.h"
  30. #include "data/data_changes.h"
  31. #include "data/data_document.h"
  32. #include "data/data_document_media.h"
  33. #include "data/data_emoji_statuses.h"
  34. #include "data/data_file_origin.h"
  35. #include "data/data_peer_bot_command.h"
  36. #include "data/data_session.h"
  37. #include "data/data_user.h"
  38. #include "data/data_web_page.h"
  39. #include "data/stickers/data_custom_emoji.h"
  40. #include "data/stickers/data_stickers.h"
  41. #include "history/history.h"
  42. #include "history/history_item.h"
  43. #include "history/history_item_helpers.h"
  44. #include "info/bot/starref/info_bot_starref_common.h" // MakePeerBubbleButton
  45. #include "info/profile/info_profile_values.h"
  46. #include "inline_bots/inline_bot_result.h"
  47. #include "inline_bots/inline_bot_confirm_prepared.h"
  48. #include "inline_bots/inline_bot_downloads.h"
  49. #include "iv/iv_instance.h"
  50. #include "lang/lang_keys.h"
  51. #include "main/main_app_config.h"
  52. #include "main/main_domain.h"
  53. #include "main/main_session.h"
  54. #include "mainwidget.h"
  55. #include "payments/payments_checkout_process.h"
  56. #include "payments/payments_non_panel_process.h"
  57. #include "settings/settings_premium.h"
  58. #include "storage/storage_account.h"
  59. #include "storage/storage_domain.h"
  60. #include "ui/basic_click_handlers.h"
  61. #include "ui/boxes/confirm_box.h"
  62. #include "ui/chat/attach/attach_bot_webview.h"
  63. #include "ui/controls/location_picker.h"
  64. #include "ui/controls/userpic_button.h"
  65. #include "ui/effects/ripple_animation.h"
  66. #include "ui/painter.h"
  67. #include "ui/text/text_utilities.h"
  68. #include "ui/toast/toast.h"
  69. #include "ui/vertical_list.h"
  70. #include "ui/widgets/checkbox.h"
  71. #include "ui/widgets/dropdown_menu.h"
  72. #include "ui/widgets/label_with_custom_emoji.h"
  73. #include "ui/widgets/menu/menu_item_base.h"
  74. #include "ui/widgets/popup_menu.h"
  75. #include "webview/webview_interface.h"
  76. #include "window/themes/window_theme.h"
  77. #include "window/window_controller.h"
  78. #include "window/window_peer_menu.h"
  79. #include "window/window_session_controller.h"
  80. #include "styles/style_boxes.h"
  81. #include "styles/style_channel_earn.h"
  82. #include "styles/style_chat.h"
  83. #include "styles/style_info.h" // infoVerifiedCheck.
  84. #include "styles/style_layers.h"
  85. #include "styles/style_menu_icons.h"
  86. #include "styles/style_window.h"
  87. #include <QSvgRenderer>
  88. namespace InlineBots {
  89. namespace {
  90. constexpr auto kProlongTimeout = 60 * crl::time(1000);
  91. constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000);
  92. constexpr auto kPopularAppBotsLimit = 100;
  93. [[nodiscard]] DocumentData *ResolveIcon(
  94. not_null<Main::Session*> session,
  95. const MTPDattachMenuBot &data) {
  96. for (const auto &icon : data.vicons().v) {
  97. const auto document = icon.match([&](
  98. const MTPDattachMenuBotIcon &data
  99. ) -> DocumentData* {
  100. if (data.vname().v == "default_static") {
  101. return session->data().processDocument(data.vicon()).get();
  102. }
  103. return nullptr;
  104. });
  105. if (document) {
  106. return document;
  107. }
  108. }
  109. return nullptr;
  110. }
  111. [[nodiscard]] PeerTypes ResolvePeerTypes(
  112. const QVector<MTPAttachMenuPeerType> &types) {
  113. auto result = PeerTypes();
  114. for (const auto &type : types) {
  115. result |= type.match([&](const MTPDattachMenuPeerTypeSameBotPM &) {
  116. return PeerType::SameBot;
  117. }, [&](const MTPDattachMenuPeerTypeBotPM &) {
  118. return PeerType::Bot;
  119. }, [&](const MTPDattachMenuPeerTypePM &) {
  120. return PeerType::User;
  121. }, [&](const MTPDattachMenuPeerTypeChat &) {
  122. return PeerType::Group;
  123. }, [&](const MTPDattachMenuPeerTypeBroadcast &) {
  124. return PeerType::Broadcast;
  125. });
  126. }
  127. return result;
  128. }
  129. [[nodiscard]] std::optional<AttachWebViewBot> ParseAttachBot(
  130. not_null<Main::Session*> session,
  131. const MTPAttachMenuBot &bot) {
  132. auto result = bot.match([&](const MTPDattachMenuBot &data) {
  133. const auto user = session->data().userLoaded(UserId(data.vbot_id()));
  134. const auto good = user
  135. && user->isBot()
  136. && user->botInfo->supportsAttachMenu;
  137. return good
  138. ? AttachWebViewBot{
  139. .user = user,
  140. .icon = ResolveIcon(session, data),
  141. .name = qs(data.vshort_name()),
  142. .types = (data.vpeer_types()
  143. ? ResolvePeerTypes(data.vpeer_types()->v)
  144. : PeerTypes()),
  145. .inactive = data.is_inactive(),
  146. .inMainMenu = data.is_show_in_side_menu(),
  147. .inAttachMenu = data.is_show_in_attach_menu(),
  148. .disclaimerRequired = data.is_side_menu_disclaimer_needed(),
  149. .requestWriteAccess = data.is_request_write_access(),
  150. } : std::optional<AttachWebViewBot>();
  151. });
  152. if (result && result->icon) {
  153. result->icon->forceToCache(true);
  154. }
  155. if (const auto icon = result->icon) {
  156. result->media = icon->createMediaView();
  157. icon->save(Data::FileOrigin(), {});
  158. }
  159. return result;
  160. }
  161. [[nodiscard]] PeerTypes PeerTypesFromNames(
  162. const std::vector<QString> &names) {
  163. auto result = PeerTypes();
  164. for (const auto &name : names) {
  165. //, bots, groups, channels
  166. result |= (name == u"users"_q)
  167. ? PeerType::User
  168. : name == u"bots"_q
  169. ? PeerType::Bot
  170. : name == u"groups"_q
  171. ? PeerType::Group
  172. : name == u"channels"_q
  173. ? PeerType::Broadcast
  174. : PeerType(0);
  175. }
  176. return result;
  177. }
  178. [[nodiscard]] Ui::LocationPickerConfig ResolveMapsConfig(
  179. not_null<Main::Session*> session) {
  180. const auto &appConfig = session->appConfig();
  181. auto map = appConfig.get<base::flat_map<QString, QString>>(
  182. u"tdesktop_config_map"_q,
  183. base::flat_map<QString, QString>());
  184. return {
  185. .mapsToken = map[u"maps"_q],
  186. .geoToken = map[u"geo"_q],
  187. };
  188. }
  189. [[nodiscard]] Window::SessionController *WindowForThread(
  190. base::weak_ptr<Window::SessionController> weak,
  191. not_null<Data::Thread*> thread) {
  192. if (const auto separate = Core::App().separateWindowFor(thread)) {
  193. return separate->sessionController();
  194. }
  195. const auto strong = weak.get();
  196. if (strong && strong->windowId().hasChatsList()) {
  197. strong->showThread(thread);
  198. return strong;
  199. }
  200. const auto window = Core::App().ensureSeparateWindowFor(thread);
  201. return window ? window->sessionController() : nullptr;
  202. }
  203. void ShowChooseBox(
  204. std::shared_ptr<Ui::Show> show,
  205. not_null<Main::Session*> session,
  206. PeerTypes types,
  207. Fn<void(not_null<Data::Thread*>)> callback,
  208. rpl::producer<QString> titleOverride = nullptr) {
  209. const auto weak = std::make_shared<QPointer<Ui::BoxContent>>();
  210. auto done = [=](not_null<Data::Thread*> thread) mutable {
  211. if (const auto strong = *weak) {
  212. strong->closeBox();
  213. }
  214. callback(thread);
  215. };
  216. auto filter = [=](not_null<Data::Thread*> thread) -> bool {
  217. const auto peer = thread->peer();
  218. if (!Data::CanSend(thread, ChatRestriction::SendInline, false)) {
  219. return false;
  220. } else if (const auto user = peer->asUser()) {
  221. if (user->isBot()) {
  222. return (types & PeerType::Bot);
  223. } else {
  224. return (types & PeerType::User);
  225. }
  226. } else if (peer->isBroadcast()) {
  227. return (types & PeerType::Broadcast);
  228. } else {
  229. return (types & PeerType::Group);
  230. }
  231. };
  232. auto initBox = [=](not_null<PeerListBox*> box) {
  233. if (titleOverride) {
  234. box->setTitle(std::move(titleOverride));
  235. }
  236. box->addButton(tr::lng_cancel(), [box] {
  237. box->closeBox();
  238. });
  239. };
  240. *weak = show->show(Box<PeerListBox>(
  241. std::make_unique<ChooseRecipientBoxController>(
  242. session,
  243. std::move(done),
  244. std::move(filter)),
  245. std::move(initBox)));
  246. }
  247. void ShowChooseBox(
  248. not_null<Window::SessionController*> controller,
  249. PeerTypes types,
  250. Fn<void(not_null<Data::Thread*>)> callback,
  251. rpl::producer<QString> titleOverride = nullptr) {
  252. ShowChooseBox(
  253. controller->uiShow(),
  254. &controller->session(),
  255. types,
  256. std::move(callback),
  257. std::move(titleOverride));
  258. }
  259. void FillDisclaimerBox(
  260. not_null<Ui::GenericBox*> box,
  261. Fn<void(bool accepted)> done) {
  262. const auto updateCheck = std::make_shared<Fn<void()>>();
  263. const auto validateCheck = std::make_shared<Fn<bool()>>();
  264. const auto callback = [=](Fn<void()> close) {
  265. if (validateCheck && (*validateCheck)()) {
  266. done(true);
  267. close();
  268. }
  269. };
  270. const auto padding = st::boxRowPadding;
  271. Ui::ConfirmBox(box, {
  272. .text = tr::lng_mini_apps_disclaimer_text(
  273. tr::now,
  274. Ui::Text::RichLangValue),
  275. .confirmed = callback,
  276. .cancelled = [=](Fn<void()> close) { done(false); close(); },
  277. .confirmText = tr::lng_box_ok(),
  278. .labelPadding = QMargins(padding.left(), 0, padding.right(), 0),
  279. .title = tr::lng_mini_apps_disclaimer_title(),
  280. });
  281. auto checkView = std::make_unique<Ui::CheckView>(
  282. st::defaultCheck,
  283. false,
  284. [=] { if (*updateCheck) { (*updateCheck)(); } });
  285. const auto check = checkView.get();
  286. const auto row = box->addRow(
  287. object_ptr<Ui::Checkbox>(
  288. box.get(),
  289. tr::lng_mini_apps_disclaimer_button(
  290. lt_link,
  291. rpl::single(Ui::Text::Link(
  292. tr::lng_mini_apps_disclaimer_link(tr::now),
  293. tr::lng_mini_apps_tos_url(tr::now))),
  294. Ui::Text::WithEntities),
  295. st::urlAuthCheckbox,
  296. std::move(checkView)),
  297. {
  298. st::boxRowPadding.left(),
  299. st::boxRowPadding.left(),
  300. st::boxRowPadding.right(),
  301. 0,
  302. });
  303. row->setAllowTextLines();
  304. row->setClickHandlerFilter([=](
  305. const ClickHandlerPtr &link,
  306. Qt::MouseButton button) {
  307. ActivateClickHandler(row, link, ClickContext{
  308. .button = button,
  309. .other = QVariant::fromValue(ClickHandlerContext{
  310. .show = box->uiShow(),
  311. })
  312. });
  313. return false;
  314. });
  315. (*updateCheck) = [=] { row->update(); };
  316. const auto showError = Ui::CheckView::PrepareNonToggledError(
  317. check,
  318. box->lifetime());
  319. (*validateCheck) = [=] {
  320. if (check->checked()) {
  321. return true;
  322. }
  323. showError();
  324. return false;
  325. };
  326. }
  327. WebViewContext ResolveContext(
  328. not_null<UserData*> bot,
  329. WebViewContext context) {
  330. if (!context.dialogsEntryState.key) {
  331. if (const auto strong = context.controller.get()) {
  332. context.dialogsEntryState = strong->dialogsEntryStateCurrent();
  333. }
  334. }
  335. if (!context.action) {
  336. const auto &state = context.dialogsEntryState;
  337. if (const auto thread = state.key.thread()) {
  338. context.action = Api::SendAction(thread);
  339. context.action->replyTo = state.currentReplyTo;
  340. } else {
  341. context.action = Api::SendAction(bot->owner().history(bot));
  342. }
  343. }
  344. if (!context.dialogsEntryState.key) {
  345. using namespace Dialogs;
  346. using Section = EntryState::Section;
  347. const auto history = context.action->history;
  348. const auto topicId = context.action->replyTo.topicRootId;
  349. const auto topic = history->peer->forumTopicFor(topicId);
  350. context.dialogsEntryState = EntryState{
  351. .key = (topic ? Key{ topic } : Key{ history }),
  352. .section = (topic ? Section::Replies : Section::History),
  353. .currentReplyTo = context.action->replyTo,
  354. };
  355. }
  356. return context;
  357. }
  358. void FillBotUsepic(
  359. not_null<Ui::GenericBox*> box,
  360. not_null<PeerData*> bot,
  361. base::weak_ptr<Window::SessionController> weak) {
  362. auto aboutLabel = Ui::CreateLabelWithCustomEmoji(
  363. box->verticalLayout(),
  364. tr::lng_allow_bot_webview_details(
  365. lt_emoji,
  366. rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
  367. Ui::Text::RichLangValue
  368. ) | rpl::map([](TextWithEntities text) {
  369. return Ui::Text::Link(std::move(text), u"internal:"_q);
  370. }),
  371. Core::TextContext({ .session = &bot->session() }),
  372. st::defaultFlatLabel);
  373. const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
  374. box->verticalLayout(),
  375. bot,
  376. st::infoPersonalChannelUserpic);
  377. Ui::AddSkip(box->verticalLayout());
  378. aboutLabel->setClickHandlerFilter([=](auto &&...) {
  379. if (const auto strong = weak.get()) {
  380. strong->showPeerHistory(
  381. bot->id,
  382. Window::SectionShow::Way::Forward);
  383. return true;
  384. }
  385. return false;
  386. });
  387. const auto title = Ui::CreateChild<Ui::RpWidget>(box->verticalLayout());
  388. const auto titleLabel = Ui::CreateChild<Ui::FlatLabel>(
  389. title,
  390. rpl::single(bot->name()),
  391. box->getDelegate()->style().title);
  392. const auto icon = bot->isVerified() ? &st::infoVerifiedCheck : nullptr;
  393. title->resize(
  394. titleLabel->width() + (icon ? icon->width() : 0),
  395. titleLabel->height());
  396. title->widthValue(
  397. ) | rpl::distinct_until_changed() | rpl::start_with_next([=](int w) {
  398. titleLabel->resizeToWidth(w
  399. - (icon ? icon->width() + st::lineWidth : 0));
  400. }, title->lifetime());
  401. if (icon) {
  402. title->paintRequest(
  403. ) | rpl::start_with_next([=] {
  404. auto p = Painter(title);
  405. p.fillRect(title->rect(), Qt::transparent);
  406. icon->paint(
  407. p,
  408. std::min(
  409. titleLabel->textMaxWidth() + st::lineWidth,
  410. title->width() - st::lineWidth - icon->width()),
  411. (title->height() - icon->height()) / 2,
  412. title->width());
  413. }, title->lifetime());
  414. }
  415. Ui::IconWithTitle(box->verticalLayout(), userpic, title, aboutLabel);
  416. }
  417. std::unique_ptr<Ui::RpWidget> MakeEmojiSetStatusPreview(
  418. not_null<QWidget*> parent,
  419. not_null<PeerData*> peer,
  420. not_null<DocumentData*> document) {
  421. const auto emoji = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(
  422. parent,
  423. object_ptr<Ui::FlatLabel>(
  424. parent,
  425. rpl::single(
  426. Ui::Text::SingleCustomEmoji(
  427. Data::SerializeCustomEmojiId(document->id),
  428. (document->sticker()
  429. ? document->sticker()->alt
  430. : QString()))),
  431. st::botEmojiStatusEmoji,
  432. st::defaultPopupMenu,
  433. Core::TextContext({ .session = &peer->session() })),
  434. style::margins(st::normalFont->spacew, 0, 0, 0));
  435. emoji->entity()->resizeToWidth(emoji->entity()->textMaxWidth());
  436. auto result = Info::BotStarRef::MakePeerBubbleButton(
  437. parent,
  438. peer,
  439. emoji);
  440. result->setAttribute(Qt::WA_TransparentForMouseEvents);
  441. return result;
  442. }
  443. bool CheckEmojiStatusPremium(not_null<UserData*> bot) {
  444. if (bot->session().premium()) {
  445. return true;
  446. }
  447. const auto window = ChatHelpers::ResolveWindowDefault()(
  448. &bot->session());
  449. if (window) {
  450. ShowPremiumPreviewBox(window, PremiumFeature::EmojiStatus);
  451. window->window().activate();
  452. }
  453. return false;
  454. }
  455. void ConfirmEmojiStatusAccessBox(
  456. not_null<Ui::GenericBox*> box,
  457. not_null<UserData*> bot,
  458. Fn<void(bool)> done) {
  459. box->setNoContentMargin(true);
  460. const auto set = box->lifetime().make_state<bool>();
  461. box->addTopButton(st::boxTitleClose, [=] {
  462. box->closeBox();
  463. });
  464. AddSkip(box->verticalLayout(), 4 * st::defaultVerticalListSkip);
  465. const auto statusIcon = ChatHelpers::GenerateLocalTgsSticker(
  466. &bot->session(),
  467. u"hello_status"_q);
  468. statusIcon->overrideEmojiUsesTextColor(true);
  469. auto ownedSet = MakeEmojiSetStatusPreview(
  470. box,
  471. bot->session().user(),
  472. statusIcon);
  473. box->addRow(
  474. object_ptr<Ui::RpWidget>::fromRaw(ownedSet.release()));
  475. AddSkip(box->verticalLayout(), 2 * st::defaultVerticalListSkip);
  476. auto name = Ui::Text::Bold(bot->name());
  477. box->addRow(object_ptr<Ui::FlatLabel>(
  478. box,
  479. tr::lng_bot_emoji_status_access_text(
  480. lt_bot,
  481. rpl::single(name),
  482. lt_name,
  483. rpl::single(name),
  484. Ui::Text::RichLangValue),
  485. st::botEmojiStatusText));
  486. box->addButton(tr::lng_bot_emoji_status_access_allow(), [=] {
  487. if (!CheckEmojiStatusPremium(bot)) {
  488. return;
  489. }
  490. *set = true;
  491. box->closeBox();
  492. done(true);
  493. });
  494. box->addButton(tr::lng_cancel(), [=] {
  495. const auto was = *set;
  496. box->closeBox();
  497. if (!was) {
  498. done(false);
  499. }
  500. });
  501. }
  502. void ConfirmEmojiStatusBox(
  503. not_null<Ui::GenericBox*> box,
  504. not_null<UserData*> bot,
  505. not_null<DocumentData*> document,
  506. TimeId duration,
  507. Fn<void(bool)> done) {
  508. box->setNoContentMargin(true);
  509. auto owned = Settings::MakeEmojiStatusPreview(box, document);
  510. const auto preview = box->addRow(
  511. object_ptr<Ui::RpWidget>::fromRaw(owned.release()));
  512. preview->resize(preview->width(), st::botEmojiStatusPreviewHeight);
  513. const auto set = box->lifetime().make_state<bool>();
  514. box->addTopButton(st::boxTitleClose, [=] {
  515. box->closeBox();
  516. });
  517. box->addRow(object_ptr<Ui::FlatLabel>(
  518. box,
  519. tr::lng_bot_emoji_status_title(),
  520. st::botEmojiStatusTitle));
  521. AddSkip(box->verticalLayout());
  522. box->addRow(object_ptr<Ui::FlatLabel>(
  523. box,
  524. tr::lng_bot_emoji_status_text(
  525. lt_bot,
  526. rpl::single(Ui::Text::Bold(bot->name())),
  527. Ui::Text::RichLangValue),
  528. st::botEmojiStatusText));
  529. AddSkip(box->verticalLayout(), 2 * st::defaultVerticalListSkip);
  530. auto ownedSet = MakeEmojiSetStatusPreview(
  531. box,
  532. document->session().user(),
  533. document);
  534. box->addRow(
  535. object_ptr<Ui::RpWidget>::fromRaw(ownedSet.release()));
  536. box->addButton(tr::lng_bot_emoji_status_confirm(), [=] {
  537. if (!CheckEmojiStatusPremium(bot)) {
  538. return;
  539. }
  540. document->owner().emojiStatuses().set(
  541. { document->id },
  542. duration ? (base::unixtime::now() + duration) : 0);
  543. *set = true;
  544. box->closeBox();
  545. done(true);
  546. });
  547. box->addButton(tr::lng_cancel(), [=] {
  548. box->closeBox();
  549. });
  550. box->boxClosing() | rpl::start_with_next([=] {
  551. if (!*set) {
  552. done(false);
  553. }
  554. }, box->lifetime());
  555. }
  556. class BotAction final : public Ui::Menu::ItemBase {
  557. public:
  558. BotAction(
  559. not_null<Ui::RpWidget*> parent,
  560. std::shared_ptr<Ui::Show> show,
  561. const style::Menu &st,
  562. const AttachWebViewBot &bot,
  563. Fn<void()> callback);
  564. bool isEnabled() const override;
  565. not_null<QAction*> action() const override;
  566. [[nodiscard]] rpl::producer<bool> forceShown() const;
  567. void handleKeyPress(not_null<QKeyEvent*> e) override;
  568. private:
  569. void contextMenuEvent(QContextMenuEvent *e) override;
  570. QPoint prepareRippleStartPosition() const override;
  571. QImage prepareRippleMask() const override;
  572. int contentHeight() const override;
  573. void prepare();
  574. void paint(Painter &p);
  575. const std::shared_ptr<Ui::Show> _show;
  576. const not_null<QAction*> _dummyAction;
  577. const style::Menu &_st;
  578. const AttachWebViewBot _bot;
  579. MenuBotIcon _icon;
  580. base::unique_qptr<Ui::PopupMenu> _menu;
  581. rpl::event_stream<bool> _forceShown;
  582. Ui::Text::String _text;
  583. int _textWidth = 0;
  584. const int _height;
  585. };
  586. BotAction::BotAction(
  587. not_null<Ui::RpWidget*> parent,
  588. std::shared_ptr<Ui::Show> show,
  589. const style::Menu &st,
  590. const AttachWebViewBot &bot,
  591. Fn<void()> callback)
  592. : ItemBase(parent, st)
  593. , _show(std::move(show))
  594. , _dummyAction(new QAction(parent))
  595. , _st(st)
  596. , _bot(bot)
  597. , _icon(this, _bot.media)
  598. , _height(_st.itemPadding.top()
  599. + _st.itemStyle.font->height
  600. + _st.itemPadding.bottom()) {
  601. setAcceptBoth(false);
  602. initResizeHook(parent->sizeValue());
  603. setClickedCallback(std::move(callback));
  604. _icon.move(_st.itemIconPosition);
  605. paintRequest(
  606. ) | rpl::start_with_next([=] {
  607. Painter p(this);
  608. paint(p);
  609. }, lifetime());
  610. enableMouseSelecting();
  611. prepare();
  612. }
  613. void BotAction::paint(Painter &p) {
  614. const auto selected = isSelected();
  615. if (selected && _st.itemBgOver->c.alpha() < 255) {
  616. p.fillRect(0, 0, width(), _height, _st.itemBg);
  617. }
  618. p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);
  619. if (isEnabled()) {
  620. paintRipple(p, 0, 0);
  621. }
  622. p.setPen(selected ? _st.itemFgOver : _st.itemFg);
  623. _text.drawLeftElided(
  624. p,
  625. _st.itemPadding.left(),
  626. _st.itemPadding.top(),
  627. _textWidth,
  628. width());
  629. }
  630. void BotAction::prepare() {
  631. _text.setMarkedText(_st.itemStyle, { _bot.name });
  632. const auto textWidth = _text.maxWidth();
  633. const auto &padding = _st.itemPadding;
  634. const auto goodWidth = padding.left()
  635. + textWidth
  636. + padding.right();
  637. const auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax);
  638. _textWidth = w - (goodWidth - textWidth);
  639. setMinWidth(w);
  640. update();
  641. }
  642. bool BotAction::isEnabled() const {
  643. return true;
  644. }
  645. not_null<QAction*> BotAction::action() const {
  646. return _dummyAction;
  647. }
  648. void BotAction::contextMenuEvent(QContextMenuEvent *e) {
  649. _menu = nullptr;
  650. _menu = base::make_unique_q<Ui::PopupMenu>(
  651. this,
  652. st::popupMenuWithIcons);
  653. _menu->addAction(tr::lng_bot_remove_from_menu(tr::now), [=] {
  654. const auto bot = _bot.user;
  655. bot->session().attachWebView().removeFromMenu(_show, bot);
  656. }, &st::menuIconDelete);
  657. QObject::connect(_menu, &QObject::destroyed, [=] {
  658. _forceShown.fire(false);
  659. });
  660. _forceShown.fire(true);
  661. _menu->popup(e->globalPos());
  662. e->accept();
  663. }
  664. QPoint BotAction::prepareRippleStartPosition() const {
  665. return mapFromGlobal(QCursor::pos());
  666. }
  667. QImage BotAction::prepareRippleMask() const {
  668. return Ui::RippleAnimation::RectMask(size());
  669. }
  670. int BotAction::contentHeight() const {
  671. return _height;
  672. }
  673. rpl::producer<bool> BotAction::forceShown() const {
  674. return _forceShown.events();
  675. }
  676. void BotAction::handleKeyPress(not_null<QKeyEvent*> e) {
  677. if (!isSelected()) {
  678. return;
  679. }
  680. const auto key = e->key();
  681. if (key == Qt::Key_Enter || key == Qt::Key_Return) {
  682. setClicked(Ui::Menu::TriggeredSource::Keyboard);
  683. }
  684. }
  685. } // namespace
  686. base::weak_ptr<WebViewInstance> WebViewInstance::PendingActivation;
  687. MenuBotIcon::MenuBotIcon(
  688. QWidget *parent,
  689. std::shared_ptr<Data::DocumentMedia> media)
  690. : RpWidget(parent)
  691. , _media(std::move(media)) {
  692. style::PaletteChanged(
  693. ) | rpl::start_with_next([=] {
  694. _image = QImage();
  695. update();
  696. }, lifetime());
  697. setAttribute(Qt::WA_TransparentForMouseEvents);
  698. resize(st::menuIconAdmin.size());
  699. show();
  700. }
  701. void MenuBotIcon::paintEvent(QPaintEvent *e) {
  702. validate();
  703. if (!_image.isNull()) {
  704. QPainter(this).drawImage(0, 0, _image);
  705. }
  706. }
  707. void MenuBotIcon::validate() {
  708. const auto ratio = style::DevicePixelRatio();
  709. const auto wanted = size() * ratio;
  710. if (_mask.size() != wanted) {
  711. if (!_media || !_media->loaded()) {
  712. return;
  713. }
  714. auto icon = QSvgRenderer(_media->bytes());
  715. _mask = QImage(wanted, QImage::Format_ARGB32_Premultiplied);
  716. _mask.setDevicePixelRatio(style::DevicePixelRatio());
  717. _mask.fill(Qt::transparent);
  718. if (icon.isValid()) {
  719. auto p = QPainter(&_mask);
  720. icon.render(&p, rect());
  721. p.end();
  722. _mask = Images::Colored(std::move(_mask), Qt::white);
  723. }
  724. }
  725. if (_image.isNull()) {
  726. _image = style::colorizeImage(_mask, st::menuIconColor);
  727. }
  728. }
  729. bool PeerMatchesTypes(
  730. not_null<PeerData*> peer,
  731. not_null<UserData*> bot,
  732. PeerTypes types) {
  733. if (const auto user = peer->asUser()) {
  734. return (user == bot)
  735. ? (types & PeerType::SameBot)
  736. : user->isBot()
  737. ? (types & PeerType::Bot)
  738. : (types & PeerType::User);
  739. } else if (peer->isBroadcast()) {
  740. return (types & PeerType::Broadcast);
  741. }
  742. return (types & PeerType::Group);
  743. }
  744. PeerTypes ParseChooseTypes(QStringView choose) {
  745. auto result = PeerTypes();
  746. for (const auto &entry : choose.split(QChar(' '))) {
  747. if (entry == u"users"_q) {
  748. result |= PeerType::User;
  749. } else if (entry == u"bots"_q) {
  750. result |= PeerType::Bot;
  751. } else if (entry == u"groups"_q) {
  752. result |= PeerType::Group;
  753. } else if (entry == u"channels"_q) {
  754. result |= PeerType::Broadcast;
  755. }
  756. }
  757. return result;
  758. }
  759. WebViewInstance::WebViewInstance(WebViewDescriptor &&descriptor)
  760. : _parentShow(descriptor.parentShow
  761. ? std::move(descriptor.parentShow)
  762. : descriptor.context.controller
  763. ? descriptor.context.controller.get()->uiShow()
  764. : nullptr)
  765. , _session(&descriptor.bot->session())
  766. , _bot(descriptor.bot)
  767. , _context(ResolveContext(_bot, std::move(descriptor.context)))
  768. , _button(std::move(descriptor.button))
  769. , _source(std::move(descriptor.source)) {
  770. resolve();
  771. }
  772. WebViewInstance::~WebViewInstance() {
  773. _session->api().request(base::take(_requestId)).cancel();
  774. _session->api().request(base::take(_prolongId)).cancel();
  775. base::take(_panel);
  776. }
  777. Main::Session &WebViewInstance::session() const {
  778. return *_session;
  779. }
  780. not_null<UserData*> WebViewInstance::bot() const {
  781. return _bot;
  782. }
  783. WebViewSource WebViewInstance::source() const {
  784. return _source;
  785. }
  786. void WebViewInstance::activate() {
  787. if (_panel) {
  788. _panel->requestActivate();
  789. } else {
  790. PendingActivation = this;
  791. }
  792. }
  793. void WebViewInstance::resolve() {
  794. v::match(_source, [&](WebViewSourceButton data) {
  795. confirmOpen([=] {
  796. if (data.simple) {
  797. requestSimple();
  798. } else {
  799. requestButton();
  800. }
  801. });
  802. }, [&](WebViewSourceSwitch) {
  803. confirmOpen([=] {
  804. requestSimple();
  805. });
  806. }, [&](WebViewSourceLinkApp data) {
  807. resolveApp(
  808. data.appname,
  809. data.token,
  810. (_context.maySkipConfirmation
  811. ? ConfirmType::None
  812. : ConfirmType::Always));
  813. }, [&](WebViewSourceLinkBotProfile) {
  814. confirmOpen([=] {
  815. requestMain();
  816. });
  817. }, [&](WebViewSourceLinkAttachMenu data) {
  818. requestWithMenuAdd();
  819. }, [&](WebViewSourceMainMenu) {
  820. requestWithMainMenuDisclaimer();
  821. }, [&](WebViewSourceAttachMenu) {
  822. requestWithMenuAdd();
  823. }, [&](WebViewSourceBotMenu) {
  824. if (!openAppFromBotMenuLink()) {
  825. confirmOpen([=] {
  826. requestButton();
  827. });
  828. }
  829. }, [&](WebViewSourceGame game) {
  830. showGame();
  831. }, [&](WebViewSourceBotProfile) {
  832. if (_context.maySkipConfirmation) {
  833. requestMain();
  834. } else {
  835. confirmOpen([=] {
  836. requestMain();
  837. });
  838. }
  839. });
  840. }
  841. bool WebViewInstance::openAppFromBotMenuLink() {
  842. const auto url = QString::fromUtf8(_button.url);
  843. const auto local = Core::TryConvertUrlToLocal(url);
  844. const auto prefix = u"tg://resolve?"_q;
  845. if (!local.startsWith(prefix)) {
  846. return false;
  847. }
  848. const auto params = qthelp::url_parse_params(
  849. local.mid(prefix.size()),
  850. qthelp::UrlParamNameTransform::ToLower);
  851. const auto domainParam = params.value(u"domain"_q);
  852. const auto appnameParam = params.value(u"appname"_q);
  853. const auto webChannelPreviewLink = (domainParam == u"s"_q)
  854. && !appnameParam.isEmpty();
  855. const auto appname = webChannelPreviewLink ? QString() : appnameParam;
  856. if (appname.isEmpty()) {
  857. return false;
  858. }
  859. resolveApp(appname, params.value(u"startapp"_q), ConfirmType::Once);
  860. return true;
  861. }
  862. void WebViewInstance::resolveApp(
  863. const QString &appname,
  864. const QString &startparam,
  865. ConfirmType confirmType) {
  866. const auto already = _session->data().findBotApp(_bot->id, appname);
  867. _requestId = _session->api().request(MTPmessages_GetBotApp(
  868. MTP_inputBotAppShortName(
  869. _bot->inputUser,
  870. MTP_string(appname)),
  871. MTP_long(already ? already->hash : 0)
  872. )).done([=](const MTPmessages_BotApp &result) {
  873. _requestId = 0;
  874. const auto &data = result.data();
  875. const auto received = _session->data().processBotApp(
  876. _bot->id,
  877. data.vapp());
  878. _app = received ? received : already;
  879. _appStartParam = startparam;
  880. if (!_app) {
  881. _parentShow->showToast(tr::lng_username_app_not_found(tr::now));
  882. close();
  883. return;
  884. }
  885. const auto confirm = data.is_inactive()
  886. || (confirmType != ConfirmType::None);
  887. const auto writeAccess = result.data().is_request_write_access();
  888. const auto forceConfirmation = data.is_inactive()
  889. || (confirmType == ConfirmType::Always);
  890. // Check if this app can be added to main menu.
  891. // On fail it'll still be opened.
  892. using Result = AttachWebView::AddToMenuResult;
  893. const auto done = crl::guard(this, [=](Result value, auto) {
  894. if (value == Result::Cancelled) {
  895. close();
  896. } else if (value != Result::Unsupported) {
  897. requestApp(true);
  898. } else if (confirm) {
  899. confirmAppOpen(writeAccess, [=](bool allowWrite) {
  900. requestApp(allowWrite);
  901. }, forceConfirmation);
  902. } else {
  903. requestApp(false);
  904. }
  905. });
  906. _session->attachWebView().requestAddToMenu(_bot, done);
  907. }).fail([=] {
  908. _parentShow->showToast(tr::lng_username_app_not_found(tr::now));
  909. close();
  910. }).send();
  911. }
  912. void WebViewInstance::confirmOpen(Fn<void()> done) {
  913. if (_bot->isVerified()
  914. || _session->local().isPeerTrustedOpenWebView(_bot->id)) {
  915. done();
  916. return;
  917. }
  918. const auto callback = [=](Fn<void()> close) {
  919. _session->local().markPeerTrustedOpenWebView(_bot->id);
  920. close();
  921. done();
  922. };
  923. const auto cancel = [=](Fn<void()> close) {
  924. botClose();
  925. close();
  926. };
  927. _parentShow->show(Box([=](not_null<Ui::GenericBox*> box) {
  928. FillBotUsepic(box, _bot, _context.controller);
  929. Ui::ConfirmBox(box, {
  930. .text = tr::lng_profile_open_app_about(
  931. tr::now,
  932. lt_terms,
  933. Ui::Text::Link(
  934. tr::lng_profile_open_app_terms(tr::now),
  935. tr::lng_mini_apps_tos_url(tr::now)),
  936. Ui::Text::RichLangValue),
  937. .confirmed = crl::guard(this, callback),
  938. .cancelled = crl::guard(this, cancel),
  939. .confirmText = tr::lng_view_button_bot_app(),
  940. });
  941. }));
  942. }
  943. void WebViewInstance::confirmAppOpen(
  944. bool writeAccess,
  945. Fn<void(bool allowWrite)> done,
  946. bool forceConfirmation) {
  947. if (!forceConfirmation
  948. && (_bot->isVerified()
  949. || _session->local().isPeerTrustedOpenWebView(_bot->id))) {
  950. done(writeAccess);
  951. return;
  952. }
  953. _parentShow->show(Box([=](not_null<Ui::GenericBox*> box) {
  954. const auto allowed = std::make_shared<Ui::Checkbox*>();
  955. const auto callback = [=](Fn<void()> close) {
  956. _session->local().markPeerTrustedOpenWebView(_bot->id);
  957. done((*allowed) && (*allowed)->checked());
  958. close();
  959. };
  960. const auto cancelled = [=](Fn<void()> close) {
  961. botClose();
  962. close();
  963. };
  964. FillBotUsepic(box, _bot, _context.controller);
  965. Ui::ConfirmBox(box, {
  966. tr::lng_profile_open_app_about(
  967. tr::now,
  968. lt_terms,
  969. Ui::Text::Link(
  970. tr::lng_profile_open_app_terms(tr::now),
  971. tr::lng_mini_apps_tos_url(tr::now)),
  972. Ui::Text::RichLangValue),
  973. crl::guard(this, callback),
  974. crl::guard(this, cancelled),
  975. tr::lng_view_button_bot_app(),
  976. });
  977. if (writeAccess) {
  978. (*allowed) = box->addRow(
  979. object_ptr<Ui::Checkbox>(
  980. box,
  981. tr::lng_url_auth_allow_messages(
  982. tr::now,
  983. lt_bot,
  984. Ui::Text::Bold(_bot->name()),
  985. Ui::Text::WithEntities),
  986. true,
  987. st::urlAuthCheckbox),
  988. style::margins(
  989. st::boxRowPadding.left(),
  990. st::boxPhotoCaptionSkip,
  991. st::boxRowPadding.right(),
  992. st::boxPhotoCaptionSkip));
  993. (*allowed)->setAllowTextLines();
  994. }
  995. }));
  996. }
  997. void WebViewInstance::requestButton() {
  998. Expects(_context.action.has_value());
  999. const auto &action = *_context.action;
  1000. using Flag = MTPmessages_RequestWebView::Flag;
  1001. _requestId = _session->api().request(MTPmessages_RequestWebView(
  1002. MTP_flags(Flag::f_theme_params
  1003. | (_context.fullscreen ? Flag::f_fullscreen : Flag(0))
  1004. | (_button.url.isEmpty() ? Flag(0) : Flag::f_url)
  1005. | (_button.startCommand.isEmpty()
  1006. ? Flag(0)
  1007. : Flag::f_start_param)
  1008. | (v::is<WebViewSourceBotMenu>(_source)
  1009. ? Flag::f_from_bot_menu
  1010. : Flag(0))
  1011. | (action.replyTo ? Flag::f_reply_to : Flag(0))
  1012. | (action.options.sendAs ? Flag::f_send_as : Flag(0))
  1013. | (action.options.silent ? Flag::f_silent : Flag(0))),
  1014. action.history->peer->input,
  1015. _bot->inputUser,
  1016. MTP_bytes(_button.url),
  1017. MTP_string(_button.startCommand),
  1018. MTP_dataJSON(MTP_bytes(botThemeParams().json)),
  1019. MTP_string("tdesktop"),
  1020. action.mtpReplyTo(),
  1021. (action.options.sendAs
  1022. ? action.options.sendAs->input
  1023. : MTP_inputPeerEmpty())
  1024. )).done([=](const MTPWebViewResult &result) {
  1025. const auto &data = result.data();
  1026. show({
  1027. .url = qs(data.vurl()),
  1028. .queryId = data.vquery_id().value_or_empty(),
  1029. .fullscreen = data.is_fullscreen(),
  1030. });
  1031. }).fail([=](const MTP::Error &error) {
  1032. _parentShow->showToast(error.type());
  1033. if (error.type() == u"BOT_INVALID"_q) {
  1034. _session->attachWebView().requestBots();
  1035. }
  1036. close();
  1037. }).send();
  1038. }
  1039. void WebViewInstance::requestSimple() {
  1040. using Flag = MTPmessages_RequestSimpleWebView::Flag;
  1041. _requestId = _session->api().request(MTPmessages_RequestSimpleWebView(
  1042. MTP_flags(Flag::f_theme_params
  1043. | (_context.fullscreen ? Flag::f_fullscreen : Flag(0))
  1044. | (v::is<WebViewSourceSwitch>(_source)
  1045. ? (Flag::f_url | Flag::f_from_switch_webview)
  1046. : v::is<WebViewSourceMainMenu>(_source)
  1047. ? (Flag::f_from_side_menu
  1048. | (_button.startCommand.isEmpty() // from LinkMainMenu
  1049. ? Flag()
  1050. : Flag::f_start_param))
  1051. : Flag::f_url)),
  1052. _bot->inputUser,
  1053. MTP_bytes(_button.url),
  1054. MTP_string(_button.startCommand),
  1055. MTP_dataJSON(MTP_bytes(botThemeParams().json)),
  1056. MTP_string("tdesktop")
  1057. )).done([=](const MTPWebViewResult &result) {
  1058. const auto &data = result.data();
  1059. show({
  1060. .url = qs(data.vurl()),
  1061. .fullscreen = data.is_fullscreen(),
  1062. });
  1063. }).fail([=](const MTP::Error &error) {
  1064. _parentShow->showToast(error.type());
  1065. close();
  1066. }).send();
  1067. }
  1068. void WebViewInstance::requestMain() {
  1069. using Flag = MTPmessages_RequestMainWebView::Flag;
  1070. _requestId = _session->api().request(MTPmessages_RequestMainWebView(
  1071. MTP_flags(Flag::f_theme_params
  1072. | (_context.fullscreen ? Flag::f_fullscreen : Flag(0))
  1073. | (_button.startCommand.isEmpty()
  1074. ? Flag()
  1075. : Flag::f_start_param)
  1076. | (v::is<WebViewSourceLinkBotProfile>(_source)
  1077. ? (v::get<WebViewSourceLinkBotProfile>(_source).compact
  1078. ? Flag::f_compact
  1079. : Flag(0))
  1080. : Flag(0))),
  1081. _context.action->history->peer->input,
  1082. _bot->inputUser,
  1083. MTP_string(_button.startCommand),
  1084. MTP_dataJSON(MTP_bytes(botThemeParams().json)),
  1085. MTP_string("tdesktop")
  1086. )).done([=](const MTPWebViewResult &result) {
  1087. const auto &data = result.data();
  1088. show({
  1089. .url = qs(data.vurl()),
  1090. .fullscreen = data.is_fullscreen(),
  1091. });
  1092. }).fail([=](const MTP::Error &error) {
  1093. _parentShow->showToast(error.type());
  1094. close();
  1095. }).send();
  1096. }
  1097. void WebViewInstance::requestApp(bool allowWrite) {
  1098. Expects(_app != nullptr);
  1099. Expects(_context.action.has_value());
  1100. using Flag = MTPmessages_RequestAppWebView::Flag;
  1101. const auto app = _app;
  1102. const auto title = app->title;
  1103. const auto flags = Flag::f_theme_params
  1104. | (_context.fullscreen ? Flag::f_fullscreen : Flag(0))
  1105. | (_appStartParam.isEmpty() ? Flag(0) : Flag::f_start_param)
  1106. | (allowWrite ? Flag::f_write_allowed : Flag(0));
  1107. _requestId = _session->api().request(MTPmessages_RequestAppWebView(
  1108. MTP_flags(flags),
  1109. _context.action->history->peer->input,
  1110. MTP_inputBotAppID(MTP_long(app->id), MTP_long(app->accessHash)),
  1111. MTP_string(_appStartParam),
  1112. MTP_dataJSON(MTP_bytes(botThemeParams().json)),
  1113. MTP_string("tdesktop")
  1114. )).done([=](const MTPWebViewResult &result) {
  1115. _requestId = 0;
  1116. const auto &data = result.data();
  1117. show({
  1118. .url = qs(data.vurl()),
  1119. .title = title,
  1120. .fullscreen = data.is_fullscreen(),
  1121. });
  1122. }).fail([=](const MTP::Error &error) {
  1123. _requestId = 0;
  1124. if (error.type() == u"BOT_INVALID"_q) {
  1125. _session->attachWebView().requestBots();
  1126. }
  1127. close();
  1128. }).send();
  1129. }
  1130. void WebViewInstance::requestWithMainMenuDisclaimer() {
  1131. using Result = AttachWebView::AddToMenuResult;
  1132. const auto done = crl::guard(this, [=](Result value, auto) {
  1133. if (value == Result::Cancelled) {
  1134. close();
  1135. } else if (value == Result::Unsupported) {
  1136. _parentShow->showToast(tr::lng_bot_menu_not_supported(tr::now));
  1137. close();
  1138. } else {
  1139. requestSimple();
  1140. }
  1141. });
  1142. _session->attachWebView().acceptMainMenuDisclaimer(
  1143. _parentShow,
  1144. _bot,
  1145. done);
  1146. }
  1147. void WebViewInstance::requestWithMenuAdd() {
  1148. using Result = AttachWebView::AddToMenuResult;
  1149. const auto done = crl::guard(this, [=](Result value, PeerTypes types) {
  1150. if (value == Result::Cancelled) {
  1151. close();
  1152. } else if (value == Result::Unsupported) {
  1153. _parentShow->showToast(tr::lng_bot_menu_not_supported(tr::now));
  1154. close();
  1155. } else if (v::is<WebViewSourceLinkAttachMenu>(_source)) {
  1156. maybeChooseAndRequestButton(types);
  1157. } else if (v::is<WebViewSourceAttachMenu>(_source)) {
  1158. requestButton();
  1159. } else {
  1160. requestSimple();
  1161. }
  1162. });
  1163. _session->attachWebView().requestAddToMenu(_bot, done);
  1164. }
  1165. void WebViewInstance::maybeChooseAndRequestButton(PeerTypes supported) {
  1166. Expects(v::is<WebViewSourceLinkAttachMenu>(_source));
  1167. const auto link = v::get<WebViewSourceLinkAttachMenu>(_source);
  1168. const auto chooseFrom = (link.choose & supported);
  1169. if (!chooseFrom) {
  1170. requestButton();
  1171. return;
  1172. }
  1173. const auto bot = _bot;
  1174. const auto button = _button;
  1175. const auto weak = _context.controller;
  1176. const auto done = [=](not_null<Data::Thread*> thread) {
  1177. if (const auto controller = WindowForThread(weak, thread)) {
  1178. thread->session().attachWebView().open({
  1179. .bot = bot,
  1180. .context = {
  1181. .controller = controller,
  1182. .action = Api::SendAction(thread),
  1183. },
  1184. .button = button,
  1185. .source = InlineBots::WebViewSourceLinkAttachMenu{
  1186. .thread = thread,
  1187. .token = button.startCommand,
  1188. },
  1189. });
  1190. }
  1191. };
  1192. ShowChooseBox(_parentShow, _session, chooseFrom, done);
  1193. close();
  1194. }
  1195. void WebViewInstance::show(ShowArgs &&args) {
  1196. auto title = args.title.isEmpty()
  1197. ? Info::Profile::NameValue(_bot)
  1198. : rpl::single(args.title);
  1199. auto titleBadge = _bot->isVerified()
  1200. ? object_ptr<Ui::RpWidget>(_parentShow->toastParent())
  1201. : nullptr;
  1202. if (titleBadge) {
  1203. const auto raw = titleBadge.data();
  1204. raw->paintRequest() | rpl::start_with_next([=] {
  1205. auto p = Painter(raw);
  1206. st::infoVerifiedCheck.paint(p, st::lineWidth, 0, raw->width());
  1207. }, raw->lifetime());
  1208. raw->resize(st::infoVerifiedCheck.size() + QSize(0, st::lineWidth));
  1209. }
  1210. const auto &bots = _session->attachWebView().attachBots();
  1211. using Button = Ui::BotWebView::MenuButton;
  1212. const auto attached = ranges::find(
  1213. bots,
  1214. not_null{ _bot },
  1215. &AttachWebViewBot::user);
  1216. const auto hasOpenBot = v::is<WebViewSourceMainMenu>(_source)
  1217. || (_context.action->history->peer != _bot);
  1218. const auto hasRemoveFromMenu = (attached != end(bots))
  1219. && (!attached->inactive || attached->inMainMenu)
  1220. && (v::is<WebViewSourceMainMenu>(_source)
  1221. || v::is<WebViewSourceAttachMenu>(_source)
  1222. || v::is<WebViewSourceLinkAttachMenu>(_source));
  1223. const auto buttons = (hasOpenBot ? Button::OpenBot : Button::None)
  1224. | (!hasRemoveFromMenu
  1225. ? Button::None
  1226. : attached->inMainMenu
  1227. ? Button::RemoveFromMainMenu
  1228. : Button::RemoveFromMenu);
  1229. const auto allowClipboardRead = v::is<WebViewSourceMainMenu>(_source)
  1230. || v::is<WebViewSourceAttachMenu>(_source)
  1231. || (attached != end(bots)
  1232. && (attached->inAttachMenu || attached->inMainMenu));
  1233. const auto downloads = &_session->attachWebView().downloads();
  1234. _panelUrl = args.url;
  1235. _panel = Ui::BotWebView::Show({
  1236. .url = args.url,
  1237. .storageId = _session->local().resolveStorageIdBots(),
  1238. .title = std::move(title),
  1239. .titleBadge = std::move(titleBadge),
  1240. .bottom = rpl::single('@' + _bot->username()),
  1241. .delegate = static_cast<Ui::BotWebView::Delegate*>(this),
  1242. .menuButtons = buttons,
  1243. .fullscreen = args.fullscreen,
  1244. .allowClipboardRead = allowClipboardRead,
  1245. .downloadsProgress = downloads->progress(_bot),
  1246. });
  1247. started(args.queryId);
  1248. if (const auto strong = PendingActivation.get()) {
  1249. if (strong == this) {
  1250. PendingActivation = nullptr;
  1251. _panel->requestActivate();
  1252. }
  1253. }
  1254. }
  1255. void WebViewInstance::showGame() {
  1256. Expects(v::is<WebViewSourceGame>(_source));
  1257. const auto game = v::get<WebViewSourceGame>(_source);
  1258. _panelUrl = QString::fromUtf8(_button.url);
  1259. _panel = Ui::BotWebView::Show({
  1260. .url = _panelUrl,
  1261. .storageId = _session->local().resolveStorageIdBots(),
  1262. .title = rpl::single(game.title),
  1263. .bottom = rpl::single('@' + _bot->username()),
  1264. .delegate = static_cast<Ui::BotWebView::Delegate*>(this),
  1265. .menuButtons = Ui::BotWebView::MenuButton::ShareGame,
  1266. });
  1267. }
  1268. void WebViewInstance::close() {
  1269. _session->attachWebView().close(this);
  1270. }
  1271. void WebViewInstance::started(uint64 queryId) {
  1272. Expects(_context.action.has_value());
  1273. if (!queryId) {
  1274. return;
  1275. }
  1276. _session->data().webViewResultSent(
  1277. ) | rpl::filter([=](const Data::Session::WebViewResultSent &sent) {
  1278. return (sent.queryId == queryId);
  1279. }) | rpl::start_with_next([=] {
  1280. close();
  1281. }, _panel->lifetime());
  1282. const auto action = *_context.action;
  1283. base::timer_each(
  1284. kProlongTimeout
  1285. ) | rpl::start_with_next([=] {
  1286. using Flag = MTPmessages_ProlongWebView::Flag;
  1287. _session->api().request(base::take(_prolongId)).cancel();
  1288. _prolongId = _session->api().request(MTPmessages_ProlongWebView(
  1289. MTP_flags(Flag(0)
  1290. | (action.replyTo ? Flag::f_reply_to : Flag(0))
  1291. | (action.options.sendAs ? Flag::f_send_as : Flag(0))
  1292. | (action.options.silent ? Flag::f_silent : Flag(0))),
  1293. action.history->peer->input,
  1294. _bot->inputUser,
  1295. MTP_long(queryId),
  1296. action.mtpReplyTo(),
  1297. (action.options.sendAs
  1298. ? action.options.sendAs->input
  1299. : MTP_inputPeerEmpty())
  1300. )).done([=] {
  1301. _prolongId = 0;
  1302. }).send();
  1303. }, _panel->lifetime());
  1304. }
  1305. Webview::ThemeParams WebViewInstance::botThemeParams() {
  1306. auto result = Window::Theme::WebViewParams();
  1307. if (const auto info = _bot->botInfo.get()) {
  1308. const auto night = Window::Theme::IsNightMode();
  1309. const auto &title = night
  1310. ? info->botAppColorTitleNight
  1311. : info->botAppColorTitleDay;
  1312. const auto &body = night
  1313. ? info->botAppColorBodyNight
  1314. : info->botAppColorBodyDay;
  1315. if (title.alpha() == 255) {
  1316. result.titleBg = title;
  1317. }
  1318. if (body.alpha() == 255) {
  1319. result.bodyBg = body;
  1320. }
  1321. }
  1322. return result;
  1323. }
  1324. auto WebViewInstance::botDownloads(bool forceCheck)
  1325. -> const std::vector<Ui::BotWebView::DownloadsEntry> & {
  1326. return _session->attachWebView().downloads().list(_bot, forceCheck);
  1327. }
  1328. void WebViewInstance::botDownloadsAction(
  1329. uint32 id,
  1330. Ui::BotWebView::DownloadsAction type) {
  1331. _session->attachWebView().downloads().action(_bot, id, type);
  1332. }
  1333. bool WebViewInstance::botHandleLocalUri(QString uri, bool keepOpen) {
  1334. const auto local = Core::TryConvertUrlToLocal(uri);
  1335. if (Core::InternalPassportLink(local)) {
  1336. return true;
  1337. } else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive)
  1338. && !local.startsWith(u"tonsite://"_q, Qt::CaseInsensitive)) {
  1339. return false;
  1340. }
  1341. const auto bot = _bot;
  1342. const auto context = std::make_shared<WebViewContext>(_context);
  1343. if (!keepOpen) {
  1344. botClose();
  1345. }
  1346. crl::on_main([=] {
  1347. if (bot->session().windows().empty()) {
  1348. Core::App().domain().activate(&bot->session().account());
  1349. }
  1350. const auto window = !bot->session().windows().empty()
  1351. ? bot->session().windows().front().get()
  1352. : nullptr;
  1353. context->controller = window;
  1354. const auto variant = QVariant::fromValue(ClickHandlerContext{
  1355. .sessionWindow = window,
  1356. .botWebviewContext = context,
  1357. });
  1358. UrlClickHandler::Open(local, variant);
  1359. });
  1360. return true;
  1361. }
  1362. void WebViewInstance::botHandleInvoice(QString slug) {
  1363. Expects(_panel != nullptr);
  1364. using Result = Payments::CheckoutResult;
  1365. const auto weak = base::make_weak(_panel.get());
  1366. const auto reactivate = [=](Result result) {
  1367. if (const auto strong = weak.get()) {
  1368. strong->invoiceClosed(slug, [&] {
  1369. switch (result) {
  1370. case Result::Paid: return "paid";
  1371. case Result::Failed: return "failed";
  1372. case Result::Pending: return "pending";
  1373. case Result::Cancelled: return "cancelled";
  1374. }
  1375. Unexpected("Payments::CheckoutResult value.");
  1376. }());
  1377. }
  1378. };
  1379. Payments::CheckoutProcess::Start(
  1380. &_bot->session(),
  1381. slug,
  1382. reactivate,
  1383. nonPanelPaymentFormFactory(reactivate));
  1384. }
  1385. auto WebViewInstance::nonPanelPaymentFormFactory(
  1386. Fn<void(Payments::CheckoutResult)> reactivate)
  1387. -> Fn<void(Payments::NonPanelPaymentForm)> {
  1388. using namespace Payments;
  1389. const auto panel = base::make_weak(_panel.get());
  1390. const auto weak = _context.controller;
  1391. return [=](Payments::NonPanelPaymentForm form) {
  1392. using CreditsFormDataPtr = std::shared_ptr<CreditsFormData>;
  1393. using CreditsReceiptPtr = std::shared_ptr<CreditsReceiptData>;
  1394. v::match(form, [&](const CreditsFormDataPtr &form) {
  1395. if (const auto strong = panel.get()) {
  1396. ProcessCreditsPayment(
  1397. uiShow(),
  1398. strong->toastParent().get(),
  1399. form,
  1400. reactivate);
  1401. }
  1402. }, [&](const CreditsReceiptPtr &receipt) {
  1403. if (const auto controller = weak.get()) {
  1404. ProcessCreditsReceipt(controller, receipt, reactivate);
  1405. }
  1406. }, [&](RealFormPresentedNotification) {
  1407. _panel->hideForPayment();
  1408. });
  1409. };
  1410. }
  1411. void WebViewInstance::botHandleMenuButton(
  1412. Ui::BotWebView::MenuButton button) {
  1413. Expects(_panel != nullptr);
  1414. using Button = Ui::BotWebView::MenuButton;
  1415. const auto bot = _bot;
  1416. switch (button) {
  1417. case Button::OpenBot:
  1418. botClose();
  1419. if (bot->session().windows().empty()) {
  1420. Core::App().domain().activate(&bot->session().account());
  1421. }
  1422. if (!bot->session().windows().empty()) {
  1423. const auto window = bot->session().windows().front();
  1424. window->showPeerHistory(bot);
  1425. window->window().activate();
  1426. }
  1427. break;
  1428. case Button::RemoveFromMenu:
  1429. case Button::RemoveFromMainMenu: {
  1430. const auto &bots = _session->attachWebView().attachBots();
  1431. const auto attached = ranges::find(
  1432. bots,
  1433. _bot,
  1434. &AttachWebViewBot::user);
  1435. const auto name = (attached != end(bots))
  1436. ? attached->name
  1437. : _bot->name();
  1438. const auto done = crl::guard(this, [=] {
  1439. const auto session = _session;
  1440. const auto was = _parentShow;
  1441. botClose();
  1442. const auto active = Core::App().activeWindow();
  1443. const auto show = active ? active->uiShow() : was;
  1444. session->attachWebView().removeFromMenu(show, bot);
  1445. if (active) {
  1446. active->activate();
  1447. }
  1448. });
  1449. const auto main = (button == Button::RemoveFromMainMenu);
  1450. _panel->showBox(Ui::MakeConfirmBox({
  1451. (main
  1452. ? tr::lng_bot_remove_from_side_menu_sure
  1453. : tr::lng_bot_remove_from_menu_sure)(
  1454. tr::now,
  1455. lt_bot,
  1456. Ui::Text::Bold(name),
  1457. Ui::Text::WithEntities),
  1458. done,
  1459. }));
  1460. } break;
  1461. case Button::ShareGame: {
  1462. const auto itemId = v::is<WebViewSourceGame>(_source)
  1463. ? v::get<WebViewSourceGame>(_source).messageId
  1464. : FullMsgId();
  1465. if (!_panel || !itemId) {
  1466. return;
  1467. } else if (const auto item = _session->data().message(itemId)) {
  1468. FastShareMessage(uiShow(), item);
  1469. } else {
  1470. _panel->showToast({ tr::lng_message_not_found(tr::now) });
  1471. }
  1472. } break;
  1473. }
  1474. }
  1475. bool WebViewInstance::botValidateExternalLink(QString uri) {
  1476. const auto lower = uri.toLower();
  1477. const auto allowed = _session->appConfig().get<std::vector<QString>>(
  1478. "web_app_allowed_protocols",
  1479. std::vector{ u"http"_q, u"https"_q });
  1480. for (const auto &protocol : allowed) {
  1481. if (lower.startsWith(protocol + u"://"_q)) {
  1482. return true;
  1483. }
  1484. }
  1485. return false;
  1486. }
  1487. void WebViewInstance::botOpenIvLink(QString uri) {
  1488. const auto window = _context.controller.get();
  1489. if (window) {
  1490. Core::App().iv().openWithIvPreferred(window, uri);
  1491. } else {
  1492. Core::App().iv().openWithIvPreferred(_session, uri);
  1493. }
  1494. }
  1495. void WebViewInstance::botSendData(QByteArray data) {
  1496. Expects(_context.action.has_value());
  1497. const auto button = std::get_if<WebViewSourceButton>(&_source);
  1498. if (!button
  1499. || !button->simple
  1500. || _context.action->history->peer != _bot
  1501. || _dataSent) {
  1502. return;
  1503. }
  1504. _dataSent = true;
  1505. _session->api().request(MTPmessages_SendWebViewData(
  1506. _bot->inputUser,
  1507. MTP_long(base::RandomValue<uint64>()),
  1508. MTP_string(_button.text),
  1509. MTP_bytes(data)
  1510. )).done([session = _session](const MTPUpdates &result) {
  1511. session->api().applyUpdates(result);
  1512. }).send();
  1513. botClose();
  1514. }
  1515. void WebViewInstance::botSwitchInlineQuery(
  1516. std::vector<QString> chatTypes,
  1517. QString query) {
  1518. const auto controller = _context.controller.get();
  1519. const auto types = PeerTypesFromNames(chatTypes);
  1520. if (!_bot
  1521. || !_bot->isBot()
  1522. || _bot->botInfo->inlinePlaceholder.isEmpty()
  1523. || !controller) {
  1524. return;
  1525. } else if (!types) {
  1526. if (_context.dialogsEntryState.key.owningHistory()) {
  1527. controller->switchInlineQuery(
  1528. _context.dialogsEntryState,
  1529. _bot,
  1530. query);
  1531. }
  1532. } else {
  1533. const auto bot = _bot;
  1534. const auto done = [=](not_null<Data::Thread*> thread) {
  1535. controller->switchInlineQuery(thread, bot, query);
  1536. };
  1537. ShowChooseBox(
  1538. controller,
  1539. types,
  1540. done,
  1541. tr::lng_inline_switch_choose());
  1542. }
  1543. botClose();
  1544. }
  1545. void WebViewInstance::botCheckWriteAccess(Fn<void(bool allowed)> callback) {
  1546. _session->api().request(MTPbots_CanSendMessage(
  1547. _bot->inputUser
  1548. )).done([=](const MTPBool &result) {
  1549. callback(mtpIsTrue(result));
  1550. }).fail([=] {
  1551. callback(false);
  1552. }).send();
  1553. }
  1554. void WebViewInstance::botAllowWriteAccess(Fn<void(bool allowed)> callback) {
  1555. _session->api().request(MTPbots_AllowSendMessage(
  1556. _bot->inputUser
  1557. )).done([session = _session, callback](const MTPUpdates &result) {
  1558. session->api().applyUpdates(result);
  1559. callback(true);
  1560. }).fail([=] {
  1561. callback(false);
  1562. }).send();
  1563. }
  1564. void WebViewInstance::botRequestEmojiStatusAccess(
  1565. Fn<void(bool allowed)> callback) {
  1566. if (_bot->botInfo->canManageEmojiStatus) {
  1567. callback(true);
  1568. } else if (const auto panel = _panel.get()) {
  1569. const auto bot = _bot;
  1570. panel->showBox(Box(ConfirmEmojiStatusAccessBox, bot, [=](bool ok) {
  1571. if (!ok) {
  1572. callback(false);
  1573. return;
  1574. }
  1575. const auto session = &bot->session();
  1576. bot->botInfo->canManageEmojiStatus = true;
  1577. session->api().request(MTPbots_ToggleUserEmojiStatusPermission(
  1578. bot->inputUser,
  1579. MTP_bool(true)
  1580. )).done([=] {
  1581. callback(true);
  1582. }).fail([=] {
  1583. callback(false);
  1584. }).send();
  1585. }));
  1586. } else {
  1587. callback(false);
  1588. }
  1589. }
  1590. void WebViewInstance::botSharePhone(Fn<void(bool shared)> callback) {
  1591. const auto history = _bot->owner().history(_bot);
  1592. if (_bot->isBlocked()) {
  1593. const auto done = crl::guard(this, [=](bool success) {
  1594. if (success) {
  1595. botSharePhone(callback);
  1596. } else {
  1597. callback(false);
  1598. }
  1599. });
  1600. _session->api().blockedPeers().unblock(_bot, done);
  1601. return;
  1602. }
  1603. auto action = Api::SendAction(history);
  1604. action.clearDraft = false;
  1605. _session->api().shareContact(
  1606. _session->user(),
  1607. action,
  1608. std::move(callback));
  1609. }
  1610. void WebViewInstance::botInvokeCustomMethod(
  1611. Ui::BotWebView::CustomMethodRequest request) {
  1612. const auto callback = request.callback;
  1613. _session->api().request(MTPbots_InvokeWebViewCustomMethod(
  1614. _bot->inputUser,
  1615. MTP_string(request.method),
  1616. MTP_dataJSON(MTP_bytes(request.params))
  1617. )).done([=](const MTPDataJSON &result) {
  1618. callback(result.data().vdata().v);
  1619. }).fail([=](const MTP::Error &error) {
  1620. callback(base::make_unexpected(error.type()));
  1621. }).send();
  1622. }
  1623. void WebViewInstance::botSendPreparedMessage(
  1624. Ui::BotWebView::SendPreparedMessageRequest request) {
  1625. const auto bot = _bot;
  1626. const auto id = request.id;
  1627. const auto panel = _panel.get();
  1628. const auto weak = base::make_weak(panel);
  1629. const auto callback = request.callback;
  1630. if (!panel) {
  1631. callback(u"UNKNOWN_ERROR"_q);
  1632. return;
  1633. }
  1634. _session->api().request(MTPmessages_GetPreparedInlineMessage(
  1635. bot->inputUser,
  1636. MTP_string(request.id)
  1637. )).done([=](const MTPmessages_PreparedInlineMessage &result) {
  1638. const auto panel = weak.get();
  1639. const auto &data = result.data();
  1640. bot->owner().processUsers(data.vusers());
  1641. const auto parsed = std::shared_ptr<Result>(Result::Create(
  1642. &bot->session(),
  1643. data.vquery_id().v,
  1644. data.vresult()));
  1645. if (!parsed || !panel) {
  1646. callback(u"UNKNOWN_ERROR"_q);
  1647. return;
  1648. }
  1649. const auto types = PeerTypesFromMTP(data.vpeer_types());
  1650. const auto history = bot->owner().history(bot->session().user());
  1651. const auto item = parsed->makeMessage(history, {
  1652. .id = bot->owner().nextNonHistoryEntryId(),
  1653. .flags = MessageFlag::FakeHistoryItem,
  1654. .from = bot->session().userPeerId(),
  1655. .date = base::unixtime::now(),
  1656. .viaBotId = peerToUser(bot->id),
  1657. });
  1658. struct State {
  1659. QPointer<Ui::BoxContent> preview;
  1660. QPointer<Ui::BoxContent> choose;
  1661. rpl::event_stream<not_null<Data::Thread*>> recipient;
  1662. Fn<void(Api::SendOptions)> send;
  1663. SendPaymentHelper sendPayment;
  1664. bool sent = false;
  1665. };
  1666. const auto state = std::make_shared<State>();
  1667. auto recipient = state->recipient.events();
  1668. const auto send = [=](
  1669. std::vector<not_null<Data::Thread*>> list,
  1670. Api::SendOptions options) {
  1671. if (state->sent) {
  1672. return;
  1673. }
  1674. state->sent = true;
  1675. const auto failed = std::make_shared<int>();
  1676. const auto count = int(list.size());
  1677. const auto weak1 = state->preview;
  1678. const auto weak2 = state->choose;
  1679. const auto close = [=] {
  1680. if (const auto strong = weak1.data()) {
  1681. strong->closeBox();
  1682. }
  1683. if (const auto strong = weak2.data()) {
  1684. strong->closeBox();
  1685. }
  1686. };
  1687. const auto done = [=](bool success) {
  1688. if (*failed < 0) {
  1689. return;
  1690. }
  1691. if (success) {
  1692. *failed = -1;
  1693. if (const auto strong2 = weak2.data()) {
  1694. strong2->showToast({ tr::lng_share_done(tr::now) });
  1695. } else if (const auto strong1 = weak1.data()) {
  1696. strong1->showToast({ tr::lng_share_done(tr::now) });
  1697. }
  1698. base::call_delayed(Ui::Toast::kDefaultDuration, close);
  1699. callback(QString());
  1700. } else if (++*failed == count) {
  1701. close();
  1702. callback(u"MESSAGE_SEND_FAILED"_q);
  1703. }
  1704. };
  1705. for (const auto &thread : list) {
  1706. bot->session().api().sendInlineResult(
  1707. bot,
  1708. parsed.get(),
  1709. Api::SendAction(thread, options),
  1710. std::nullopt,
  1711. done);
  1712. }
  1713. };
  1714. auto box = Box(PreparedPreviewBox, item, std::move(recipient), [=] {
  1715. if (state->sent) {
  1716. return;
  1717. }
  1718. const auto chosen = [=](not_null<Data::Thread*> thread) {
  1719. if (!Data::CanSend(thread, ChatRestriction::SendInline)) {
  1720. panel->showToast({
  1721. tr::lng_restricted_send_inline_all(tr::now),
  1722. });
  1723. return false;
  1724. }
  1725. state->recipient.fire_copy(thread);
  1726. return true;
  1727. };
  1728. auto box = Window::PrepareChooseRecipientBox(
  1729. &bot->session(),
  1730. chosen,
  1731. tr::lng_inline_switch_choose(),
  1732. nullptr,
  1733. types,
  1734. send);
  1735. state->choose = box.data();
  1736. panel->showBox(std::move(box));
  1737. }, [=](not_null<Data::Thread*> thread) {
  1738. const auto weak = base::make_weak(thread);
  1739. state->send = [=](Api::SendOptions options) {
  1740. const auto strong = weak.get();
  1741. if (!strong) {
  1742. state->send = nullptr;
  1743. return;
  1744. }
  1745. const auto withPaymentApproved = [=](int stars) {
  1746. if (const auto onstack = state->send) {
  1747. auto copy = options;
  1748. copy.starsApproved = stars;
  1749. onstack(copy);
  1750. }
  1751. };
  1752. const auto checked = state->sendPayment.check(
  1753. uiShow(),
  1754. strong->peer(),
  1755. 1,
  1756. options.starsApproved,
  1757. withPaymentApproved);
  1758. if (!checked) {
  1759. return;
  1760. }
  1761. state->send = nullptr;
  1762. send({ strong }, options);
  1763. };
  1764. state->send({});
  1765. });
  1766. box->boxClosing() | rpl::start_with_next([=] {
  1767. if (!state->sent) {
  1768. callback("USER_DECLINED");
  1769. }
  1770. }, box->lifetime());
  1771. state->preview = box.data();
  1772. panel->showBox(std::move(box));
  1773. }).fail([=] {
  1774. callback(u"MESSAGE_EXPIRED"_q);
  1775. }).send();
  1776. }
  1777. void WebViewInstance::botSetEmojiStatus(
  1778. Ui::BotWebView::SetEmojiStatusRequest request) {
  1779. const auto bot = _bot;
  1780. const auto panel = _panel.get();
  1781. const auto callback = request.callback;
  1782. const auto duration = request.duration;
  1783. if (!panel) {
  1784. callback(u"UNKNOWN_ERROR"_q);
  1785. return;
  1786. }
  1787. _session->data().customEmojiManager().resolve(
  1788. request.customEmojiId
  1789. ) | rpl::start_with_next_error([=](not_null<DocumentData*> document) {
  1790. const auto sticker = document->sticker();
  1791. if (!sticker || sticker->setType != Data::StickersType::Emoji) {
  1792. callback(u"SUGGESTED_EMOJI_INVALID"_q);
  1793. return;
  1794. }
  1795. const auto done = [=](bool success) {
  1796. callback(success ? QString() : u"USER_DECLINED"_q);
  1797. };
  1798. panel->showBox(
  1799. Box(ConfirmEmojiStatusBox, bot, document, duration, done));
  1800. }, [=] { callback(u"SUGGESTED_EMOJI_INVALID"_q); }, panel->lifetime());
  1801. }
  1802. void WebViewInstance::botDownloadFile(
  1803. Ui::BotWebView::DownloadFileRequest request) {
  1804. const auto callback = request.callback;
  1805. if (_confirmingDownload || !_panel) {
  1806. callback(false);
  1807. return;
  1808. }
  1809. _confirmingDownload = true;
  1810. const auto done = [=](QString path) {
  1811. _confirmingDownload = false;
  1812. if (path.isEmpty()) {
  1813. callback(false);
  1814. return;
  1815. }
  1816. _bot->session().attachWebView().downloads().start({
  1817. .bot = _bot,
  1818. .url = request.url,
  1819. .path = path,
  1820. });
  1821. callback(true);
  1822. };
  1823. _bot->session().api().request(MTPbots_CheckDownloadFileParams(
  1824. _bot->inputUser,
  1825. MTP_string(request.name),
  1826. MTP_string(request.url)
  1827. )).done([=] {
  1828. _panel->showBox(Box(DownloadFileBox, DownloadBoxArgs{
  1829. .session = &_bot->session(),
  1830. .bot = _bot->name(),
  1831. .name = base::FileNameFromUserString(request.name),
  1832. .url = request.url,
  1833. .done = done,
  1834. }));
  1835. }).fail([=] {
  1836. done(QString());
  1837. }).send();
  1838. }
  1839. void WebViewInstance::botOpenPrivacyPolicy() {
  1840. const auto bot = _bot;
  1841. const auto weak = _context.controller;
  1842. const auto command = u"privacy"_q;
  1843. const auto findCommand = [=] {
  1844. if (!bot->isBot()) {
  1845. return QString();
  1846. }
  1847. for (const auto &data : bot->botInfo->commands) {
  1848. const auto isSame = !data.command.compare(
  1849. command,
  1850. Qt::CaseInsensitive);
  1851. if (isSame) {
  1852. return data.command;
  1853. }
  1854. }
  1855. return QString();
  1856. };
  1857. const auto makeOtherContext = [=](bool forceWindow) {
  1858. return QVariant::fromValue(ClickHandlerContext{
  1859. .sessionWindow = (forceWindow
  1860. ? WindowForThread(weak, bot->owner().history(bot))
  1861. : weak),
  1862. .peer = bot,
  1863. });
  1864. };
  1865. const auto sendCommand = [=] {
  1866. const auto original = findCommand();
  1867. if (original.isEmpty()) {
  1868. return false;
  1869. }
  1870. BotCommandClickHandler('/' + original).onClick(ClickContext{
  1871. Qt::LeftButton,
  1872. makeOtherContext(true)
  1873. });
  1874. return true;
  1875. };
  1876. const auto openUrl = [=](const QString &url) {
  1877. Core::App().iv().openWithIvPreferred(
  1878. &_bot->session(),
  1879. url,
  1880. makeOtherContext(false));
  1881. };
  1882. if (const auto info = _bot->botInfo.get()) {
  1883. if (!info->privacyPolicyUrl.isEmpty()) {
  1884. openUrl(info->privacyPolicyUrl);
  1885. return;
  1886. }
  1887. }
  1888. if (!sendCommand()) {
  1889. openUrl(tr::lng_profile_bot_privacy_url(tr::now));
  1890. }
  1891. }
  1892. void WebViewInstance::botClose() {
  1893. crl::on_main(this, [=] { close(); });
  1894. }
  1895. std::shared_ptr<Main::SessionShow> WebViewInstance::uiShow() {
  1896. class Show final : public Main::SessionShow {
  1897. public:
  1898. explicit Show(not_null<WebViewInstance*> that) : _that(that) {
  1899. }
  1900. void showOrHideBoxOrLayer(
  1901. std::variant<
  1902. v::null_t,
  1903. object_ptr<Ui::BoxContent>,
  1904. std::unique_ptr<Ui::LayerWidget>> &&layer,
  1905. Ui::LayerOptions options,
  1906. anim::type animated) const override {
  1907. using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
  1908. using ObjectBox = object_ptr<Ui::BoxContent>;
  1909. const auto panel = _that ? _that->_panel.get() : nullptr;
  1910. if (v::is<UniqueLayer>(layer)) {
  1911. Unexpected("Layers in WebView are not implemented.");
  1912. } else if (auto box = std::get_if<ObjectBox>(&layer)) {
  1913. if (panel) {
  1914. panel->showBox(std::move(*box), options, animated);
  1915. }
  1916. } else if (panel) {
  1917. panel->hideLayer(animated);
  1918. }
  1919. }
  1920. [[nodiscard]] not_null<QWidget*> toastParent() const override {
  1921. const auto panel = _that ? _that->_panel.get() : nullptr;
  1922. Ensures(panel != nullptr);
  1923. return panel->toastParent();
  1924. }
  1925. [[nodiscard]] bool valid() const override {
  1926. return _that && (_that->_panel != nullptr);
  1927. }
  1928. operator bool() const override {
  1929. return valid();
  1930. }
  1931. [[nodiscard]] Main::Session &session() const override {
  1932. Expects(_that.get() != nullptr);
  1933. return *_that->_session;
  1934. }
  1935. private:
  1936. const base::weak_ptr<WebViewInstance> _that;
  1937. };
  1938. return std::make_shared<Show>(this);
  1939. }
  1940. AttachWebView::AttachWebView(not_null<Main::Session*> session)
  1941. : _session(session)
  1942. , _downloads(std::make_unique<Downloads>(session))
  1943. , _refreshTimer([=] { requestBots(); }) {
  1944. _refreshTimer.callEach(kRefreshBotsTimeout);
  1945. }
  1946. AttachWebView::~AttachWebView() {
  1947. closeAll();
  1948. _session->api().request(_popularAppBotsRequestId).cancel();
  1949. }
  1950. void AttachWebView::openByUsername(
  1951. not_null<Window::SessionController*> controller,
  1952. const Api::SendAction &action,
  1953. const QString &botUsername,
  1954. const QString &startCommand,
  1955. bool fullscreen) {
  1956. if (botUsername.isEmpty()
  1957. || (_botUsername == botUsername
  1958. && _startCommand == startCommand
  1959. && _fullScreenRequested == fullscreen)) {
  1960. return;
  1961. }
  1962. cancel();
  1963. _botUsername = botUsername;
  1964. _startCommand = startCommand;
  1965. _fullScreenRequested = fullscreen;
  1966. const auto weak = base::make_weak(controller);
  1967. const auto show = controller->uiShow();
  1968. resolveUsername(show, crl::guard(weak, [=](not_null<PeerData*> peer) {
  1969. _botUsername = QString();
  1970. const auto token = base::take(_startCommand);
  1971. const auto fullscreen = base::take(_fullScreenRequested);
  1972. const auto bot = peer->asUser();
  1973. if (!bot || !bot->isBot()) {
  1974. if (const auto strong = weak.get()) {
  1975. strong->showToast(tr::lng_bot_menu_not_supported(tr::now));
  1976. }
  1977. return;
  1978. }
  1979. open({
  1980. .bot = bot,
  1981. .context = {
  1982. .controller = controller,
  1983. .action = action,
  1984. .fullscreen = fullscreen,
  1985. },
  1986. .button = { .startCommand = token },
  1987. .source = InlineBots::WebViewSourceLinkAttachMenu{},
  1988. });
  1989. }));
  1990. }
  1991. void AttachWebView::close(not_null<WebViewInstance*> instance) {
  1992. const auto i = ranges::find(
  1993. _instances,
  1994. instance.get(),
  1995. &std::unique_ptr<WebViewInstance>::get);
  1996. if (i != end(_instances)) {
  1997. const auto taken = base::take(*i);
  1998. _instances.erase(i);
  1999. }
  2000. }
  2001. void AttachWebView::closeAll() {
  2002. cancel();
  2003. base::take(_instances);
  2004. }
  2005. void AttachWebView::loadPopularAppBots() {
  2006. if (_popularAppBotsLoaded.current() || _popularAppBotsRequestId) {
  2007. return;
  2008. }
  2009. _popularAppBotsRequestId = _session->api().request(
  2010. MTPbots_GetPopularAppBots(
  2011. MTP_string(),
  2012. MTP_int(kPopularAppBotsLimit))
  2013. ).done([=](const MTPbots_PopularAppBots &result) {
  2014. _popularAppBotsRequestId = 0;
  2015. const auto &list = result.data().vusers().v;
  2016. auto parsed = std::vector<not_null<UserData*>>();
  2017. parsed.reserve(list.size());
  2018. for (const auto &user : list) {
  2019. const auto bot = _session->data().processUser(user);
  2020. if (bot->isBot()) {
  2021. parsed.push_back(bot);
  2022. }
  2023. }
  2024. _popularAppBots = std::move(parsed);
  2025. _popularAppBotsLoaded = true;
  2026. }).send();
  2027. }
  2028. auto AttachWebView::popularAppBots() const
  2029. -> const std::vector<not_null<UserData*>> & {
  2030. return _popularAppBots;
  2031. }
  2032. rpl::producer<> AttachWebView::popularAppBotsLoaded() const {
  2033. return _popularAppBotsLoaded.changes() | rpl::to_empty;
  2034. }
  2035. void AttachWebView::cancel() {
  2036. _session->api().request(base::take(_requestId)).cancel();
  2037. _botUsername = QString();
  2038. _startCommand = QString();
  2039. }
  2040. void AttachWebView::requestBots(Fn<void()> callback) {
  2041. if (callback) {
  2042. _botsRequestCallbacks.push_back(std::move(callback));
  2043. }
  2044. if (_botsRequestId) {
  2045. return;
  2046. }
  2047. _botsRequestId = _session->api().request(MTPmessages_GetAttachMenuBots(
  2048. MTP_long(_botsHash)
  2049. )).done([=](const MTPAttachMenuBots &result) {
  2050. _botsRequestId = 0;
  2051. result.match([&](const MTPDattachMenuBotsNotModified &) {
  2052. }, [&](const MTPDattachMenuBots &data) {
  2053. _session->data().processUsers(data.vusers());
  2054. _botsHash = data.vhash().v;
  2055. _attachBots.clear();
  2056. _attachBots.reserve(data.vbots().v.size());
  2057. for (const auto &bot : data.vbots().v) {
  2058. if (auto parsed = ParseAttachBot(_session, bot)) {
  2059. _attachBots.push_back(std::move(*parsed));
  2060. }
  2061. }
  2062. _attachBotsUpdates.fire({});
  2063. });
  2064. for (const auto &callback : base::take(_botsRequestCallbacks)) {
  2065. callback();
  2066. }
  2067. }).fail([=] {
  2068. _botsRequestId = 0;
  2069. for (const auto &callback : base::take(_botsRequestCallbacks)) {
  2070. callback();
  2071. }
  2072. }).send();
  2073. }
  2074. bool AttachWebView::disclaimerAccepted(const AttachWebViewBot &bot) const {
  2075. return _disclaimerAccepted.contains(bot.user);
  2076. }
  2077. bool AttachWebView::showMainMenuNewBadge(
  2078. const AttachWebViewBot &bot) const {
  2079. return bot.inMainMenu
  2080. && bot.disclaimerRequired
  2081. && !disclaimerAccepted(bot);
  2082. }
  2083. void AttachWebView::requestAddToMenu(
  2084. not_null<UserData*> bot,
  2085. Fn<void(AddToMenuResult, PeerTypes supported)> done) {
  2086. auto &process = _addToMenu[bot];
  2087. if (done) {
  2088. process.done.push_back(std::move(done));
  2089. }
  2090. if (process.requestId) {
  2091. return;
  2092. }
  2093. const auto finish = [=](AddToMenuResult result, PeerTypes supported) {
  2094. if (auto process = _addToMenu.take(bot)) {
  2095. for (const auto &done : process->done) {
  2096. done(result, supported);
  2097. }
  2098. }
  2099. };
  2100. if (!bot->isBot() || !bot->botInfo->supportsAttachMenu) {
  2101. finish(AddToMenuResult::Unsupported, {});
  2102. return;
  2103. }
  2104. process.requestId = _session->api().request(
  2105. MTPmessages_GetAttachMenuBot(bot->inputUser)
  2106. ).done([=](const MTPAttachMenuBotsBot &result) {
  2107. _addToMenu[bot].requestId = 0;
  2108. const auto &data = result.data();
  2109. _session->data().processUsers(data.vusers());
  2110. const auto parsed = ParseAttachBot(_session, data.vbot());
  2111. if (!parsed || bot != parsed->user) {
  2112. finish(AddToMenuResult::Unsupported, {});
  2113. return;
  2114. }
  2115. const auto i = ranges::find(
  2116. _attachBots,
  2117. not_null(bot),
  2118. &AttachWebViewBot::user);
  2119. if (i != end(_attachBots)) {
  2120. // Save flags in our list, like 'inactive'.
  2121. *i = *parsed;
  2122. }
  2123. const auto types = parsed->types;
  2124. if (parsed->inactive) {
  2125. confirmAddToMenu(*parsed, [=](bool added) {
  2126. const auto result = added
  2127. ? AddToMenuResult::Added
  2128. : AddToMenuResult::Cancelled;
  2129. finish(result, types);
  2130. });
  2131. } else {
  2132. requestBots();
  2133. finish(AddToMenuResult::AlreadyInMenu, types);
  2134. }
  2135. }).fail([=] {
  2136. finish(AddToMenuResult::Unsupported, {});
  2137. }).send();
  2138. }
  2139. void AttachWebView::removeFromMenu(
  2140. std::shared_ptr<Ui::Show> show,
  2141. not_null<UserData*> bot) {
  2142. toggleInMenu(bot, ToggledState::Removed, [=](bool success) {
  2143. if (success) {
  2144. show->showToast(tr::lng_bot_remove_from_menu_done(tr::now));
  2145. }
  2146. });
  2147. }
  2148. void AttachWebView::resolveUsername(
  2149. std::shared_ptr<Ui::Show> show,
  2150. Fn<void(not_null<PeerData*>)> done) {
  2151. if (const auto peer = _session->data().peerByUsername(_botUsername)) {
  2152. done(peer);
  2153. return;
  2154. }
  2155. _session->api().request(base::take(_requestId)).cancel();
  2156. _requestId = _session->api().request(MTPcontacts_ResolveUsername(
  2157. MTP_flags(0),
  2158. MTP_string(_botUsername),
  2159. MTP_string()
  2160. )).done([=](const MTPcontacts_ResolvedPeer &result) {
  2161. _requestId = 0;
  2162. result.match([&](const MTPDcontacts_resolvedPeer &data) {
  2163. _session->data().processUsers(data.vusers());
  2164. _session->data().processChats(data.vchats());
  2165. if (const auto peerId = peerFromMTP(data.vpeer())) {
  2166. done(_session->data().peer(peerId));
  2167. }
  2168. });
  2169. }).fail([=](const MTP::Error &error) {
  2170. _requestId = 0;
  2171. if (error.code() == 400) {
  2172. show->showToast(
  2173. tr::lng_username_not_found(tr::now, lt_user, _botUsername));
  2174. }
  2175. }).send();
  2176. }
  2177. void AttachWebView::open(WebViewDescriptor &&descriptor) {
  2178. for (const auto &instance : _instances) {
  2179. if (instance->bot() == descriptor.bot
  2180. && instance->source() == descriptor.source) {
  2181. instance->activate();
  2182. return;
  2183. }
  2184. }
  2185. _instances.push_back(
  2186. std::make_unique<WebViewInstance>(std::move(descriptor)));
  2187. _instances.back()->activate();
  2188. }
  2189. void AttachWebView::acceptMainMenuDisclaimer(
  2190. std::shared_ptr<Ui::Show> show,
  2191. not_null<UserData*> bot,
  2192. Fn<void(AddToMenuResult, PeerTypes supported)> done) {
  2193. const auto i = ranges::find(_attachBots, bot, &AttachWebViewBot::user);
  2194. if (i == end(_attachBots)) {
  2195. _attachBotsUpdates.fire({});
  2196. return;
  2197. } else if (i->inactive) {
  2198. requestAddToMenu(bot, std::move(done));
  2199. return;
  2200. } else if (!i->disclaimerRequired || disclaimerAccepted(*i)) {
  2201. done(AddToMenuResult::AlreadyInMenu, i->types);
  2202. return;
  2203. }
  2204. const auto types = i->types;
  2205. show->show(Box(FillDisclaimerBox, crl::guard(this, [=](bool accepted) {
  2206. if (accepted) {
  2207. _disclaimerAccepted.emplace(bot);
  2208. _attachBotsUpdates.fire({});
  2209. done(AddToMenuResult::AlreadyInMenu, types);
  2210. } else {
  2211. done(AddToMenuResult::Cancelled, {});
  2212. }
  2213. })));
  2214. }
  2215. void AttachWebView::confirmAddToMenu(
  2216. AttachWebViewBot bot,
  2217. Fn<void(bool added)> callback) {
  2218. const auto active = Core::App().activeWindow();
  2219. if (!active) {
  2220. if (callback) {
  2221. callback(false);
  2222. }
  2223. return;
  2224. }
  2225. const auto weak = base::make_weak(active);
  2226. active->show(Box([=](not_null<Ui::GenericBox*> box) {
  2227. const auto allowed = std::make_shared<Ui::Checkbox*>();
  2228. const auto disclaimer = !disclaimerAccepted(bot);
  2229. const auto done = [=](Fn<void()> close) {
  2230. const auto state = (disclaimer
  2231. || ((*allowed) && (*allowed)->checked()))
  2232. ? ToggledState::AllowedToWrite
  2233. : ToggledState::Added;
  2234. toggleInMenu(bot.user, state, [=](bool success) {
  2235. if (callback) {
  2236. callback(success);
  2237. }
  2238. if (const auto strong = weak.get()) {
  2239. strong->showToast((bot.inMainMenu
  2240. ? tr::lng_bot_add_to_side_menu_done
  2241. : tr::lng_bot_add_to_menu_done)(tr::now));
  2242. }
  2243. });
  2244. close();
  2245. };
  2246. if (disclaimer) {
  2247. FillDisclaimerBox(box, [=](bool accepted) {
  2248. if (accepted) {
  2249. _disclaimerAccepted.emplace(bot.user);
  2250. _attachBotsUpdates.fire({});
  2251. done([] {});
  2252. } else if (callback) {
  2253. callback(false);
  2254. }
  2255. });
  2256. box->addRow(object_ptr<Ui::FixedHeightWidget>(
  2257. box,
  2258. st::boxRowPadding.left()));
  2259. box->addRow(object_ptr<Ui::FlatLabel>(
  2260. box,
  2261. tr::lng_bot_will_be_added(
  2262. lt_bot,
  2263. rpl::single(Ui::Text::Bold(bot.name)),
  2264. Ui::Text::WithEntities),
  2265. st::boxLabel));
  2266. } else {
  2267. Ui::ConfirmBox(box, {
  2268. (bot.inMainMenu
  2269. ? tr::lng_bot_add_to_side_menu
  2270. : tr::lng_bot_add_to_menu)(
  2271. tr::now,
  2272. lt_bot,
  2273. Ui::Text::Bold(bot.name),
  2274. Ui::Text::WithEntities),
  2275. done,
  2276. (callback
  2277. ? [=](Fn<void()> close) { callback(false); close(); }
  2278. : Fn<void(Fn<void()>)>()),
  2279. });
  2280. if (bot.requestWriteAccess) {
  2281. (*allowed) = box->addRow(
  2282. object_ptr<Ui::Checkbox>(
  2283. box,
  2284. tr::lng_url_auth_allow_messages(
  2285. tr::now,
  2286. lt_bot,
  2287. Ui::Text::Bold(bot.name),
  2288. Ui::Text::WithEntities),
  2289. true,
  2290. st::urlAuthCheckbox),
  2291. style::margins(
  2292. st::boxRowPadding.left(),
  2293. (disclaimer
  2294. ? st::boxPhotoCaptionSkip
  2295. : st::boxRowPadding.left()),
  2296. st::boxRowPadding.right(),
  2297. st::boxRowPadding.left()));
  2298. (*allowed)->setAllowTextLines();
  2299. }
  2300. }
  2301. }));
  2302. }
  2303. void AttachWebView::toggleInMenu(
  2304. not_null<UserData*> bot,
  2305. ToggledState state,
  2306. Fn<void(bool success)> callback) {
  2307. using Flag = MTPmessages_ToggleBotInAttachMenu::Flag;
  2308. _session->api().request(MTPmessages_ToggleBotInAttachMenu(
  2309. MTP_flags((state == ToggledState::AllowedToWrite)
  2310. ? Flag::f_write_allowed
  2311. : Flag()),
  2312. bot->inputUser,
  2313. MTP_bool(state != ToggledState::Removed)
  2314. )).done([=] {
  2315. _requestId = 0;
  2316. _session->api().request(base::take(_botsRequestId)).cancel();
  2317. requestBots(callback ? [=] { callback(true); } : Fn<void()>());
  2318. }).fail([=] {
  2319. cancel();
  2320. if (callback) {
  2321. callback(false);
  2322. }
  2323. }).send();
  2324. }
  2325. void ChooseAndSendLocation(
  2326. not_null<Window::SessionController*> controller,
  2327. const Ui::LocationPickerConfig &config,
  2328. Api::SendAction action) {
  2329. const auto weak = base::make_weak(controller);
  2330. const auto session = &controller->session();
  2331. if (const auto picker = session->locationPickers().lookup(action)) {
  2332. picker->activate();
  2333. return;
  2334. }
  2335. struct State {
  2336. SendPaymentHelper sendPayment;
  2337. Fn<void(Data::InputVenue, Api::SendAction)> send;
  2338. };
  2339. const auto state = std::make_shared<State>();
  2340. state->send = [=](Data::InputVenue venue, Api::SendAction action) {
  2341. if (const auto strong = weak.get()) {
  2342. const auto withPaymentApproved = [=](int stars) {
  2343. if (const auto onstack = state->send) {
  2344. auto copy = action;
  2345. copy.options.starsApproved = stars;
  2346. onstack(venue, copy);
  2347. }
  2348. };
  2349. const auto checked = state->sendPayment.check(
  2350. strong,
  2351. action.history->peer,
  2352. 1,
  2353. action.options.starsApproved,
  2354. withPaymentApproved);
  2355. if (!checked) {
  2356. return;
  2357. }
  2358. }
  2359. state->send = nullptr;
  2360. if (venue.justLocation()) {
  2361. Api::SendLocation(action, venue.lat, venue.lon);
  2362. } else {
  2363. Api::SendVenue(action, venue);
  2364. }
  2365. };
  2366. const auto callback = [=](Data::InputVenue venue) {
  2367. state->send(venue, action);
  2368. };
  2369. const auto picker = Ui::LocationPicker::Show({
  2370. .parent = controller->widget(),
  2371. .config = config,
  2372. .chooseLabel = tr::lng_maps_point_send(),
  2373. .recipient = action.history->peer,
  2374. .session = session,
  2375. .callback = crl::guard(session, callback),
  2376. .quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },
  2377. .storageId = session->local().resolveStorageIdBots(),
  2378. .closeRequests = controller->content()->death(),
  2379. });
  2380. session->locationPickers().emplace(action, picker);
  2381. }
  2382. std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
  2383. not_null<QWidget*> parent,
  2384. not_null<Window::SessionController*> controller,
  2385. not_null<PeerData*> peer,
  2386. Fn<Api::SendAction()> actionFactory,
  2387. Fn<void(bool)> attach) {
  2388. auto result = std::make_unique<Ui::DropdownMenu>(
  2389. parent,
  2390. st::dropdownMenuWithIcons);
  2391. const auto bots = &peer->session().attachWebView();
  2392. const auto raw = result.get();
  2393. auto minimal = 0;
  2394. if (Data::CanSend(peer, ChatRestriction::SendPhotos, false)) {
  2395. ++minimal;
  2396. raw->addAction(tr::lng_attach_photo_or_video(tr::now), [=] {
  2397. attach(true);
  2398. }, &st::menuIconPhoto);
  2399. }
  2400. const auto fileTypes = ChatRestriction::SendVideos
  2401. | ChatRestriction::SendGifs
  2402. | ChatRestriction::SendStickers
  2403. | ChatRestriction::SendMusic
  2404. | ChatRestriction::SendFiles;
  2405. if (Data::CanSendAnyOf(peer, fileTypes)) {
  2406. ++minimal;
  2407. raw->addAction(tr::lng_attach_document(tr::now), [=] {
  2408. attach(false);
  2409. }, &st::menuIconFile);
  2410. }
  2411. if (peer->canCreatePolls()) {
  2412. ++minimal;
  2413. raw->addAction(tr::lng_polls_create(tr::now), [=] {
  2414. const auto action = actionFactory();
  2415. const auto source = action.options.scheduled
  2416. ? Api::SendType::Scheduled
  2417. : Api::SendType::Normal;
  2418. const auto sendMenuType = (action.replyTo.topicRootId
  2419. || action.history->peer->starsPerMessageChecked())
  2420. ? SendMenu::Type::SilentOnly
  2421. : SendMenu::Type::Scheduled;
  2422. const auto flag = PollData::Flags();
  2423. const auto replyTo = action.replyTo;
  2424. Window::PeerMenuCreatePoll(
  2425. controller,
  2426. peer,
  2427. replyTo,
  2428. flag,
  2429. flag,
  2430. source,
  2431. { sendMenuType });
  2432. }, &st::menuIconCreatePoll);
  2433. }
  2434. const auto session = &controller->session();
  2435. const auto locationType = ChatRestriction::SendOther;
  2436. const auto config = ResolveMapsConfig(session);
  2437. if (Data::CanSendAnyOf(peer, locationType)
  2438. && Ui::LocationPicker::Available(config)) {
  2439. raw->addAction(tr::lng_maps_point(tr::now), [=] {
  2440. ChooseAndSendLocation(controller, config, actionFactory());
  2441. }, &st::menuIconAddress);
  2442. }
  2443. const auto addBots = Data::CanSend(peer, ChatRestriction::SendInline)
  2444. && !peer->starsPerMessageChecked();
  2445. for (const auto &bot : bots->attachBots()) {
  2446. if (!addBots
  2447. || !bot.inAttachMenu
  2448. || !PeerMatchesTypes(peer, bot.user, bot.types)) {
  2449. continue;
  2450. }
  2451. const auto callback = [=] {
  2452. bots->open({
  2453. .bot = bot.user,
  2454. .context = {
  2455. .controller = controller,
  2456. .action = actionFactory(),
  2457. },
  2458. .source = InlineBots::WebViewSourceAttachMenu(),
  2459. });
  2460. };
  2461. auto action = base::make_unique_q<BotAction>(
  2462. raw,
  2463. controller->uiShow(),
  2464. raw->menu()->st(),
  2465. bot,
  2466. callback);
  2467. action->forceShown(
  2468. ) | rpl::start_with_next([=](bool shown) {
  2469. if (shown) {
  2470. raw->setAutoHiding(false);
  2471. } else {
  2472. raw->hideAnimated();
  2473. raw->setAutoHiding(true);
  2474. }
  2475. }, action->lifetime());
  2476. raw->addAction(std::move(action));
  2477. }
  2478. const auto actions = raw->actions().size();
  2479. const auto onclick = ChatHelpers::ShowPanelOnClick();
  2480. if (!actions) {
  2481. return nullptr;
  2482. } else if (actions <= minimal && !onclick) {
  2483. return nullptr;
  2484. }
  2485. return result;
  2486. }
  2487. } // namespace InlineBots