inline_bot_result.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  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/inline_bot_result.h"
  8. #include "api/api_text_entities.h"
  9. #include "base/random.h"
  10. #include "data/data_photo.h"
  11. #include "data/data_document.h"
  12. #include "data/data_session.h"
  13. #include "data/data_file_click_handler.h"
  14. #include "data/data_file_origin.h"
  15. #include "data/data_photo_media.h"
  16. #include "data/data_document_media.h"
  17. #include "history/history.h"
  18. #include "history/history_item.h"
  19. #include "history/history_item_reply_markup.h"
  20. #include "inline_bots/inline_bot_layout_item.h"
  21. #include "inline_bots/inline_bot_send_data.h"
  22. #include "storage/file_download.h"
  23. #include "core/file_utilities.h"
  24. #include "core/mime_type.h"
  25. #include "ui/image/image.h"
  26. #include "ui/image/image_location_factory.h"
  27. #include "mainwidget.h"
  28. #include "main/main_session.h"
  29. #include "styles/style_chat_helpers.h"
  30. namespace InlineBots {
  31. namespace {
  32. const auto kVideoThumbMime = "video/mp4"_q;
  33. QString GetContentUrl(const MTPWebDocument &document) {
  34. switch (document.type()) {
  35. case mtpc_webDocument:
  36. return qs(document.c_webDocument().vurl());
  37. case mtpc_webDocumentNoProxy:
  38. return qs(document.c_webDocumentNoProxy().vurl());
  39. }
  40. Unexpected("Type in GetContentUrl.");
  41. }
  42. } // namespace
  43. Result::Result(not_null<Main::Session*> session, const Creator &creator)
  44. : _session(session)
  45. , _queryId(creator.queryId)
  46. , _type(creator.type) {
  47. }
  48. std::shared_ptr<Result> Result::Create(
  49. not_null<Main::Session*> session,
  50. uint64 queryId,
  51. const MTPBotInlineResult &data) {
  52. using Type = Result::Type;
  53. const auto type = [&] {
  54. static const auto kStringToTypeMap = base::flat_map<QString, Type>{
  55. { u"photo"_q, Type::Photo },
  56. { u"video"_q, Type::Video },
  57. { u"audio"_q, Type::Audio },
  58. { u"voice"_q, Type::Audio },
  59. { u"sticker"_q, Type::Sticker },
  60. { u"file"_q, Type::File },
  61. { u"gif"_q, Type::Gif },
  62. { u"article"_q, Type::Article },
  63. { u"contact"_q, Type::Contact },
  64. { u"venue"_q, Type::Venue },
  65. { u"geo"_q, Type::Geo },
  66. { u"game"_q, Type::Game },
  67. };
  68. const auto type = data.match([](const auto &data) {
  69. return qs(data.vtype());
  70. });
  71. const auto i = kStringToTypeMap.find(type);
  72. return (i != kStringToTypeMap.end()) ? i->second : Type::Unknown;
  73. }();
  74. if (type == Type::Unknown) {
  75. return nullptr;
  76. }
  77. auto result = std::make_shared<Result>(
  78. session,
  79. Creator{ queryId, type });
  80. const auto message = data.match([&](const MTPDbotInlineResult &data) {
  81. result->_id = qs(data.vid());
  82. result->_title = qs(data.vtitle().value_or_empty());
  83. result->_description = qs(data.vdescription().value_or_empty());
  84. result->_url = qs(data.vurl().value_or_empty());
  85. const auto thumbMime = [&] {
  86. if (const auto thumb = data.vthumb()) {
  87. return thumb->match([&](const auto &data) {
  88. return data.vmime_type().v;
  89. });
  90. }
  91. return QByteArray();
  92. }();
  93. const auto contentMime = [&] {
  94. if (const auto content = data.vcontent()) {
  95. return content->match([&](const auto &data) {
  96. return data.vmime_type().v;
  97. });
  98. }
  99. return QByteArray();
  100. }();
  101. const auto imageThumb = !thumbMime.isEmpty()
  102. && (thumbMime != kVideoThumbMime);
  103. const auto videoThumb = !thumbMime.isEmpty() && !imageThumb;
  104. if (const auto content = data.vcontent()) {
  105. result->_content_url = GetContentUrl(*content);
  106. if (result->_type == Type::Photo) {
  107. result->_photo = session->data().photoFromWeb(
  108. *content,
  109. (imageThumb
  110. ? Images::FromWebDocument(*data.vthumb())
  111. : ImageLocation()));
  112. } else if (contentMime != "text/html"_q) {
  113. result->_document = session->data().documentFromWeb(
  114. result->adjustAttributes(*content),
  115. (imageThumb
  116. ? Images::FromWebDocument(*data.vthumb())
  117. : ImageLocation()),
  118. (videoThumb
  119. ? Images::FromWebDocument(*data.vthumb())
  120. : ImageLocation()));
  121. }
  122. }
  123. if (!result->_photo && !result->_document && imageThumb) {
  124. result->_thumbnail.update(result->_session, ImageWithLocation{
  125. .location = Images::FromWebDocument(*data.vthumb())
  126. });
  127. }
  128. return &data.vsend_message();
  129. }, [&](const MTPDbotInlineMediaResult &data) {
  130. result->_id = qs(data.vid());
  131. result->_title = qs(data.vtitle().value_or_empty());
  132. result->_description = qs(data.vdescription().value_or_empty());
  133. if (const auto photo = data.vphoto()) {
  134. result->_photo = session->data().processPhoto(*photo);
  135. }
  136. if (const auto document = data.vdocument()) {
  137. result->_document = session->data().processDocument(*document);
  138. }
  139. return &data.vsend_message();
  140. });
  141. if ((result->_photo && result->_photo->isNull())
  142. || (result->_document && result->_document->isNull())) {
  143. return nullptr;
  144. }
  145. // Ensure required media fields for layouts.
  146. if (result->_type == Type::Photo) {
  147. if (!result->_photo) {
  148. return nullptr;
  149. }
  150. } else if (result->_type == Type::Audio
  151. || result->_type == Type::File
  152. || result->_type == Type::Sticker
  153. || result->_type == Type::Gif) {
  154. if (!result->_document) {
  155. return nullptr;
  156. }
  157. }
  158. message->match([&](const MTPDbotInlineMessageMediaAuto &data) {
  159. const auto message = qs(data.vmessage());
  160. const auto entities = Api::EntitiesFromMTP(
  161. session,
  162. data.ventities().value_or_empty());
  163. if (result->_type == Type::Photo) {
  164. if (result->_photo) {
  165. result->sendData = std::make_unique<internal::SendPhoto>(
  166. session,
  167. result->_photo,
  168. message,
  169. entities);
  170. } else {
  171. LOG(("Inline Error: No 'photo' in media-auto, type=photo."));
  172. }
  173. } else if (result->_type == Type::Game) {
  174. result->createGame(session);
  175. result->sendData = std::make_unique<internal::SendGame>(
  176. session,
  177. result->_game);
  178. } else {
  179. if (result->_document) {
  180. result->sendData = std::make_unique<internal::SendFile>(
  181. session,
  182. result->_document,
  183. message,
  184. entities);
  185. } else {
  186. LOG(("Inline Error: No 'document' in media-auto, type=%1."
  187. ).arg(int(result->_type)));
  188. }
  189. }
  190. }, [&](const MTPDbotInlineMessageText &data) {
  191. result->sendData = std::make_unique<internal::SendText>(
  192. session,
  193. qs(data.vmessage()),
  194. Api::EntitiesFromMTP(session, data.ventities().value_or_empty()),
  195. data.is_no_webpage());
  196. }, [&](const MTPDbotInlineMessageMediaGeo &data) {
  197. data.vgeo().match([&](const MTPDgeoPoint &geo) {
  198. if (const auto period = data.vperiod()) {
  199. result->sendData = std::make_unique<internal::SendGeo>(
  200. session,
  201. geo,
  202. period->v,
  203. (data.vheading()
  204. ? std::make_optional(data.vheading()->v)
  205. : std::nullopt),
  206. (data.vproximity_notification_radius()
  207. ? std::make_optional(
  208. data.vproximity_notification_radius()->v)
  209. : std::nullopt));
  210. } else {
  211. result->sendData = std::make_unique<internal::SendGeo>(
  212. session,
  213. geo);
  214. }
  215. }, [&](const MTPDgeoPointEmpty &) {
  216. LOG(("Inline Error: Empty 'geo' in media-geo."));
  217. });
  218. }, [&](const MTPDbotInlineMessageMediaVenue &data) {
  219. data.vgeo().match([&](const MTPDgeoPoint &geo) {
  220. result->sendData = std::make_unique<internal::SendVenue>(
  221. session,
  222. geo,
  223. qs(data.vvenue_id()),
  224. qs(data.vprovider()),
  225. qs(data.vtitle()),
  226. qs(data.vaddress()));
  227. }, [&](const MTPDgeoPointEmpty &) {
  228. LOG(("Inline Error: Empty 'geo' in media-venue."));
  229. });
  230. }, [&](const MTPDbotInlineMessageMediaContact &data) {
  231. result->sendData = std::make_unique<internal::SendContact>(
  232. session,
  233. qs(data.vfirst_name()),
  234. qs(data.vlast_name()),
  235. qs(data.vphone_number()));
  236. }, [&](const MTPDbotInlineMessageMediaInvoice &data) {
  237. using Flag = MTPDmessageMediaInvoice::Flag;
  238. const auto media = MTP_messageMediaInvoice(
  239. MTP_flags((data.is_shipping_address_requested()
  240. ? Flag::f_shipping_address_requested
  241. : Flag(0))
  242. | (data.is_test() ? Flag::f_test : Flag(0))
  243. | (data.vphoto() ? Flag::f_photo : Flag(0))),
  244. data.vtitle(),
  245. data.vdescription(),
  246. data.vphoto() ? (*data.vphoto()) : MTPWebDocument(),
  247. MTPint(), // receipt_msg_id
  248. data.vcurrency(),
  249. data.vtotal_amount(),
  250. MTP_string(QString()), // start_param
  251. MTPMessageExtendedMedia());
  252. result->sendData = std::make_unique<internal::SendInvoice>(
  253. session,
  254. media);
  255. }, [&](const MTPDbotInlineMessageMediaWebPage &data) {
  256. result->sendData = std::make_unique<internal::SendText>(
  257. session,
  258. qs(data.vmessage()),
  259. Api::EntitiesFromMTP(session, data.ventities().value_or_empty()),
  260. false);
  261. });
  262. if (!result->sendData || !result->sendData->isValid()) {
  263. return nullptr;
  264. }
  265. message->match([&](const auto &data) {
  266. if (const auto markup = data.vreply_markup()) {
  267. result->_replyMarkup
  268. = std::make_unique<HistoryMessageMarkupData>(markup);
  269. }
  270. });
  271. if (const auto point = result->getLocationPoint()) {
  272. const auto scale = 1 + (cScale() * style::DevicePixelRatio()) / 200;
  273. const auto zoom = 15 + (scale - 1);
  274. const auto w = st::inlineThumbSize / scale;
  275. const auto h = st::inlineThumbSize / scale;
  276. auto location = GeoPointLocation();
  277. location.lat = point->lat();
  278. location.lon = point->lon();
  279. location.access = point->accessHash();
  280. location.width = w;
  281. location.height = h;
  282. location.zoom = zoom;
  283. location.scale = scale;
  284. result->_locationThumbnail.update(result->_session, ImageWithLocation{
  285. .location = ImageLocation({ location }, w, h)
  286. });
  287. }
  288. return result;
  289. }
  290. bool Result::onChoose(Layout::ItemBase *layout) {
  291. if (_photo && _type == Type::Photo) {
  292. const auto media = _photo->activeMediaView();
  293. if (!media || media->image(Data::PhotoSize::Thumbnail)) {
  294. return true;
  295. } else if (!_photo->loading(Data::PhotoSize::Thumbnail)) {
  296. _photo->load(
  297. Data::PhotoSize::Thumbnail,
  298. Data::FileOrigin());
  299. }
  300. return false;
  301. }
  302. if (_document && (
  303. _type == Type::Video ||
  304. _type == Type::Audio ||
  305. _type == Type::Sticker ||
  306. _type == Type::File ||
  307. _type == Type::Gif)) {
  308. if (_type == Type::Gif) {
  309. const auto media = _document->activeMediaView();
  310. const auto preview = Data::VideoPreviewState(media.get());
  311. if (!media || preview.loaded()) {
  312. return true;
  313. } else if (!preview.usingThumbnail()) {
  314. if (preview.loading()) {
  315. _document->cancel();
  316. } else {
  317. DocumentSaveClickHandler::Save(
  318. Data::FileOriginSavedGifs(),
  319. _document);
  320. }
  321. }
  322. return false;
  323. }
  324. return true;
  325. }
  326. return true;
  327. }
  328. Media::View::OpenRequest Result::openRequest() {
  329. using namespace Media::View;
  330. if (_document) {
  331. return OpenRequest(nullptr, _document, nullptr, MsgId());
  332. } else if (_photo) {
  333. return OpenRequest(nullptr, _photo, nullptr, MsgId());
  334. }
  335. return {};
  336. }
  337. void Result::cancelFile() {
  338. if (_document) {
  339. DocumentCancelClickHandler(_document, nullptr).onClick({});
  340. } else if (_photo) {
  341. PhotoCancelClickHandler(_photo, nullptr).onClick({});
  342. }
  343. }
  344. bool Result::hasThumbDisplay() const {
  345. if (!_thumbnail.empty()
  346. || _photo
  347. || (_document && _document->hasThumbnail())) {
  348. return true;
  349. } else if (_type == Type::Contact) {
  350. return true;
  351. } else if (sendData->hasLocationCoords()) {
  352. return true;
  353. }
  354. return false;
  355. };
  356. void Result::addToHistory(
  357. not_null<History*> history,
  358. HistoryItemCommonFields &&fields) const {
  359. history->addNewLocalMessage(makeMessage(history, std::move(fields)));
  360. }
  361. not_null<HistoryItem*> Result::makeMessage(
  362. not_null<History*> history,
  363. HistoryItemCommonFields &&fields) const {
  364. fields.flags |= MessageFlag::FromInlineBot | MessageFlag::Local;
  365. if (_replyMarkup) {
  366. fields.markup = *_replyMarkup;
  367. if (!fields.markup.isNull()) {
  368. fields.flags |= MessageFlag::HasReplyMarkup;
  369. }
  370. }
  371. return sendData->makeMessage(this, history, std::move(fields));
  372. }
  373. Data::SendError Result::getErrorOnSend(not_null<History*> history) const {
  374. return sendData->getErrorOnSend(this, history).value_or(
  375. Data::RestrictionError(history->peer, ChatRestriction::SendInline));
  376. }
  377. std::optional<Data::LocationPoint> Result::getLocationPoint() const {
  378. return sendData->getLocationPoint();
  379. }
  380. QString Result::getLayoutTitle() const {
  381. return sendData->getLayoutTitle(this);
  382. }
  383. QString Result::getLayoutDescription() const {
  384. return sendData->getLayoutDescription(this);
  385. }
  386. // just to make unique_ptr see the destructors.
  387. Result::~Result() {
  388. }
  389. void Result::createGame(not_null<Main::Session*> session) {
  390. if (_game) {
  391. return;
  392. }
  393. const auto gameId = base::RandomValue<GameId>();
  394. _game = session->data().game(
  395. gameId,
  396. 0,
  397. QString(),
  398. _title,
  399. _description,
  400. _photo,
  401. _document);
  402. }
  403. QSize Result::thumbBox() const {
  404. return (_type == Type::Photo) ? QSize(100, 100) : QSize(90, 90);
  405. }
  406. MTPWebDocument Result::adjustAttributes(const MTPWebDocument &document) {
  407. switch (document.type()) {
  408. case mtpc_webDocument: {
  409. const auto &data = document.c_webDocument();
  410. return MTP_webDocument(
  411. data.vurl(),
  412. data.vaccess_hash(),
  413. data.vsize(),
  414. data.vmime_type(),
  415. adjustAttributes(data.vattributes(), data.vmime_type()));
  416. } break;
  417. case mtpc_webDocumentNoProxy: {
  418. const auto &data = document.c_webDocumentNoProxy();
  419. return MTP_webDocumentNoProxy(
  420. data.vurl(),
  421. data.vsize(),
  422. data.vmime_type(),
  423. adjustAttributes(data.vattributes(), data.vmime_type()));
  424. } break;
  425. }
  426. Unexpected("Type in InlineBots::Result::adjustAttributes.");
  427. }
  428. MTPVector<MTPDocumentAttribute> Result::adjustAttributes(
  429. const MTPVector<MTPDocumentAttribute> &existing,
  430. const MTPstring &mimeType) {
  431. auto result = existing.v;
  432. const auto find = [&](mtpTypeId attributeType) {
  433. return ranges::find(
  434. result,
  435. attributeType,
  436. [](const MTPDocumentAttribute &value) { return value.type(); });
  437. };
  438. const auto exists = [&](mtpTypeId attributeType) {
  439. return find(attributeType) != result.cend();
  440. };
  441. const auto mime = qs(mimeType);
  442. if (_type == Type::Gif) {
  443. if (!exists(mtpc_documentAttributeFilename)) {
  444. auto filename = (mime == u"video/mp4"_q
  445. ? "animation.gif.mp4"
  446. : "animation.gif");
  447. result.push_back(MTP_documentAttributeFilename(
  448. MTP_string(filename)));
  449. }
  450. if (!exists(mtpc_documentAttributeAnimated)) {
  451. result.push_back(MTP_documentAttributeAnimated());
  452. }
  453. } else if (_type == Type::Audio) {
  454. const auto audio = find(mtpc_documentAttributeAudio);
  455. if (audio != result.cend()) {
  456. using Flag = MTPDdocumentAttributeAudio::Flag;
  457. if (mime == u"audio/ogg"_q) {
  458. // We always treat audio/ogg as a voice message.
  459. // It was that way before we started to get attributes here.
  460. const auto &fields = audio->c_documentAttributeAudio();
  461. if (!(fields.vflags().v & Flag::f_voice)) {
  462. *audio = MTP_documentAttributeAudio(
  463. MTP_flags(fields.vflags().v | Flag::f_voice),
  464. fields.vduration(),
  465. MTP_bytes(fields.vtitle().value_or_empty()),
  466. MTP_bytes(fields.vperformer().value_or_empty()),
  467. MTP_bytes(fields.vwaveform().value_or_empty()));
  468. }
  469. }
  470. const auto &fields = audio->c_documentAttributeAudio();
  471. if (!exists(mtpc_documentAttributeFilename)
  472. && !(fields.vflags().v & Flag::f_voice)) {
  473. const auto p = Core::MimeTypeForName(mime).globPatterns();
  474. auto pattern = p.isEmpty() ? QString() : p.front();
  475. const auto extension = pattern.isEmpty()
  476. ? u".unknown"_q
  477. : pattern.replace('*', QString());
  478. const auto filename = filedialogDefaultName(
  479. u"inline"_q,
  480. extension,
  481. QString(),
  482. true);
  483. result.push_back(
  484. MTP_documentAttributeFilename(MTP_string(filename)));
  485. }
  486. }
  487. }
  488. return MTP_vector<MTPDocumentAttribute>(std::move(result));
  489. }
  490. } // namespace InlineBots