From 341ab781b2ea8533da124507eec8479e869ca225 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 12 Nov 2024 21:32:21 +0400 Subject: [PATCH] Implement download of files in miniapps. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 3 + .../inline_bots/bot_attach_web_view.cpp | 45 ++- .../inline_bots/bot_attach_web_view.h | 11 +- .../inline_bots/inline_bot_confirm_prepared.h | 4 + .../inline_bots/inline_bot_downloads.cpp | 324 ++++++++++++++++++ .../inline_bots/inline_bot_downloads.h | 99 ++++++ .../SourceFiles/storage/file_download_web.cpp | 127 ++++++- .../SourceFiles/storage/file_download_web.h | 19 +- .../SourceFiles/storage/storage_account.cpp | 51 +++ .../SourceFiles/storage/storage_account.h | 5 + .../ui/chat/attach/attach_bot_webview.cpp | 40 ++- .../ui/chat/attach/attach_bot_webview.h | 20 +- Telegram/SourceFiles/ui/chat/chat.style | 2 + 14 files changed, 721 insertions(+), 31 deletions(-) create mode 100644 Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp create mode 100644 Telegram/SourceFiles/inline_bots/inline_bot_downloads.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 956115e4b..f05dcb98e 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1042,6 +1042,8 @@ PRIVATE inline_bots/bot_attach_web_view.h inline_bots/inline_bot_confirm_prepared.cpp inline_bots/inline_bot_confirm_prepared.h + inline_bots/inline_bot_downloads.cpp + inline_bots/inline_bot_downloads.h inline_bots/inline_bot_layout_internal.cpp inline_bots/inline_bot_layout_internal.h inline_bots/inline_bot_layout_item.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1525b0fed..e925aeaa6 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3428,6 +3428,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_share_prepared_title" = "Share Message"; "lng_bot_share_prepared_about" = "{bot} mini app suggests you to send this message to a chat you select."; "lng_bot_share_prepared_button" = "Share With..."; +"lng_bot_download_file" = "Download File"; +"lng_bot_download_file_sure" = "{bot} suggests you download the following file:"; +"lng_bot_download_file_button" = "Download"; "lng_bot_status_users#one" = "{count} monthly user"; "lng_bot_status_users#other" = "{count} monthly users"; diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index b2609d3ea..637ec996b 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_sending.h" #include "apiwrap.h" #include "base/call_delayed.h" +#include "base/base_file_utilities.h" #include "base/qthelp_url.h" #include "base/random.h" #include "base/timer_rpl.h" @@ -42,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_values.h" #include "inline_bots/inline_bot_result.h" #include "inline_bots/inline_bot_confirm_prepared.h" +#include "inline_bots/inline_bot_downloads.h" #include "iv/iv_instance.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" @@ -569,7 +571,7 @@ void ConfirmEmojiStatusBox( not_null box, not_null bot, not_null document, - TimeId until, + TimeId duration, Fn done) { box->setNoContentMargin(true); @@ -608,7 +610,9 @@ void ConfirmEmojiStatusBox( object_ptr::fromRaw(ownedSet.release())); box->addButton(tr::lng_bot_emoji_status_confirm(), [=] { - document->owner().emojiStatuses().set(document->id, until); + document->owner().emojiStatuses().set( + document->id, + duration ? (base::unixtime::now() + duration) : 0); *set = true; box->closeBox(); done(true); @@ -1345,6 +1349,7 @@ void WebViewInstance::show(ShowArgs &&args) { || v::is(_source) || (attached != end(bots) && (attached->inAttachMenu || attached->inMainMenu)); + const auto downloads = &_session->attachWebView().downloads(); _panelUrl = args.url; _panel = Ui::BotWebView::Show({ .url = args.url, @@ -1356,6 +1361,7 @@ void WebViewInstance::show(ShowArgs &&args) { .menuButtons = buttons, .fullscreen = args.fullscreen, .allowClipboardRead = allowClipboardRead, + .downloadsProgress = downloads->downloadsProgress(_bot), }); started(args.queryId); @@ -1862,7 +1868,7 @@ void WebViewInstance::botSetEmojiStatus( const auto bot = _bot; const auto panel = _panel.get(); const auto callback = request.callback; - const auto until = request.expirationDate; + const auto duration = request.duration; if (!panel) { callback(u"UNKNOWN_ERROR"_q); return; @@ -1879,10 +1885,40 @@ void WebViewInstance::botSetEmojiStatus( callback(success ? QString() : u"USER_DECLINED"_q); }; panel->showBox( - Box(ConfirmEmojiStatusBox, bot, document, until, done)); + Box(ConfirmEmojiStatusBox, bot, document, duration, done)); }, [=] { callback(u"SUGGESTED_EMOJI_INVALID"_q); }, panel->lifetime()); } +void WebViewInstance::botDownloadFile( + Ui::BotWebView::DownloadFileRequest request) { + const auto callback = request.callback; + if (_confirmingDownload || !_panel) { + callback(false); + return; + } + _confirmingDownload = true; + const auto done = [=](QString path) { + _confirmingDownload = false; + if (path.isEmpty()) { + callback(false); + return; + } + _bot->session().attachWebView().downloads().start({ + .bot = _bot, + .url = request.url, + .path = path, + }); + callback(true); + }; + _panel->showBox(Box(DownloadFileBox, DownloadBoxArgs{ + .session = &_bot->session(), + .bot = _bot->name(), + .name = base::FileNameFromUserString(request.name), + .url = request.url, + .done = done, + })); +} + void WebViewInstance::botOpenPrivacyPolicy() { const auto bot = _bot; const auto weak = _context.controller; @@ -1995,6 +2031,7 @@ std::shared_ptr WebViewInstance::uiShow() { AttachWebView::AttachWebView(not_null session) : _session(session) +, _downloads(std::make_unique(session)) , _refreshTimer([=] { requestBots(); }) { _refreshTimer.callEach(kRefreshBotsTimeout); } diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 46e1487c4..9b3e7b500 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -7,11 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "api/api_common.h" #include "base/flags.h" #include "base/timer.h" #include "base/weak_ptr.h" #include "dialogs/dialogs_key.h" -#include "api/api_common.h" #include "mtproto/sender.h" #include "ui/chat/attach/attach_bot_webview.h" #include "ui/rp_widget.h" @@ -51,6 +51,7 @@ enum class CheckoutResult; namespace InlineBots { class WebViewInstance; +class Downloads; enum class PeerType : uint8 { SameBot = 0x01, @@ -269,6 +270,8 @@ private: Ui::BotWebView::SendPreparedMessageRequest request) override; void botSetEmojiStatus( Ui::BotWebView::SetEmojiStatusRequest request) override; + void botDownloadFile( + Ui::BotWebView::DownloadFileRequest request) override; void botOpenPrivacyPolicy() override; void botClose() override; @@ -282,6 +285,7 @@ private: BotAppData *_app = nullptr; QString _appStartParam; bool _dataSent = false; + bool _confirmingDownload = false; mtpRequestId _requestId = 0; mtpRequestId _prolongId = 0; @@ -300,6 +304,10 @@ public: explicit AttachWebView(not_null session); ~AttachWebView(); + [[nodiscard]] Downloads &downloads() const { + return *_downloads; + } + void open(WebViewDescriptor &&descriptor); void openByUsername( not_null controller, @@ -370,6 +378,7 @@ private: Fn callback = nullptr); const not_null _session; + const std::unique_ptr _downloads; base::Timer _refreshTimer; diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.h b/Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.h index 69af5d11a..c2169a7cc 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.h @@ -11,6 +11,10 @@ namespace Data { class Thread; } // namespace Data +namespace Main { +class Session; +} // namespace Main + namespace Ui { class GenericBox; } // namespace Ui diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp new file mode 100644 index 000000000..40644f6e5 --- /dev/null +++ b/Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp @@ -0,0 +1,324 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "inline_bots/inline_bot_downloads.h" + +#include "data/data_document.h" +#include "data/data_peer_id.h" +#include "data/data_user.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "storage/file_download_web.h" +#include "storage/serialize_common.h" +#include "storage/storage_account.h" +#include "ui/chat/attach/attach_bot_webview.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/labels.h" +#include "styles/style_chat.h" + +#include +#include + +namespace InlineBots { +namespace { + +constexpr auto kDownloadsVersion = 1; +constexpr auto kMaxDownloadsBots = 4096; +constexpr auto kMaxDownloadsPerBot = 16384; + +} // namespace + +Downloads::Downloads(not_null session) +: _session(session) { +} + +Downloads::~Downloads() = default; + +DownloadId Downloads::start(StartArgs &&args) { + read(); + + const auto botId = args.bot->id; + const auto id = ++_autoIncrementId; + auto &list = _lists[botId].list; + list.push_back({ + .id = id, + .url = std::move(args.url), + .path = std::move(args.path), + }); + auto &entry = list.back(); + auto &loader = _loaders[id]; + loader.botId = botId; + loader.loader = std::make_unique( + _session, + entry.url, + entry.path, + WebRequestType::FullLoad); + loader.loader->updates( + ) | rpl::start_with_next_error_done([=] { + progress(botId, id); + }, [=](FileLoader::Error) { + fail(botId, id); + }, [=] { + done(botId, id); + }, loader.loader->lifetime()); + loader.loader->start(); + + return id; +} + +void Downloads::progress(PeerId botId, DownloadId id) { + const auto i = _loaders.find(id); + if (i == end(_loaders)) { + return; + } + const auto &loader = i->second.loader; + const auto total = loader->fullSize(); + const auto ready = loader->currentOffset(); + + auto &list = _lists[botId].list; + const auto j = ranges::find( + list, + id, + &DownloadsEntry::id); + Assert(j != end(list)); + + if (total < 0 + || ready > total + || (j->total && j->total != total)) { + fail(botId, id); + return; + } else if (ready > total) { + fail(botId, id); + return; + } else if (ready == total) { + // Wait for 'done' signal. + return; + } + applyProgress(botId, id, total, ready); +} + +void Downloads::fail(PeerId botId, DownloadId id) { + const auto i = _loaders.find(id); + if (i == end(_loaders)) { + return; + } + auto loader = std::move(i->second.loader); + _loaders.erase(i); + loader = nullptr; + + auto &list = _lists[botId].list; + const auto k = ranges::find( + list, + id, + &DownloadsEntry::id); + Assert(k != end(list)); + k->ready = -1; +} + +void Downloads::done(PeerId botId, DownloadId id) { + const auto i = _loaders.find(id); + if (i == end(_loaders)) { + return; + } + auto &list = _lists[botId].list; + const auto j = ranges::find( + list, + id, + &DownloadsEntry::id); + Assert(j != end(list)); + + const auto total = i->second.loader->fullSize(); + if (total <= 0 || (j->total && j->total != total)) { + fail(botId, id); + return; + } + _loaders.erase(i); + + applyProgress(botId, id, total, total); +} + +void Downloads::applyProgress( + PeerId botId, + DownloadId id, + int64 total, + int64 ready) { + Expects(total > 0); + Expects(ready >= 0 && ready <= total); + + auto &list = _lists[botId].list; + const auto j = ranges::find( + list, + id, + &DownloadsEntry::id); + Assert(j != end(list)); + + auto &progress = _progressView[botId]; + auto current = progress.current(); + if (!j->total) { + j->total = total; + current.total += total; + } + if (j->ready != ready) { + const auto delta = ready - j->ready; + j->ready = ready; + current.ready += delta; + } + + if (total == ready) { + write(); + } + + progress = current; + if (current.ready == current.total) { + progress = DownloadsProgress(); + } +} + +void Downloads::cancel(DownloadId id) { + const auto i = _loaders.find(id); + if (i == end(_loaders)) { + return; + } + const auto botId = i->second.botId; + fail(botId, id); + + auto &list = _lists[botId].list; + list.erase( + ranges::remove(list, id, &DownloadsEntry::id), + end(list)); + + auto &progress = _progressView[botId]; + progress.force_assign(progress.current()); +} + +[[nodiscard]] auto Downloads::downloadsProgress(not_null bot) +->rpl::producer { + read(); + + return _progressView[bot->id].value(); +} + +void Downloads::read() { + auto bytes = _session->local().readInlineBotsDownloads(); + if (bytes.isEmpty()) { + return; + } + + Assert(_lists.empty()); + + auto stream = QDataStream(&bytes, QIODevice::ReadOnly); + stream.setVersion(QDataStream::Qt_5_1); + + quint32 version = 0, count = 0; + stream >> version; + if (version != kDownloadsVersion) { + return; + } + stream >> count; + if (!count || count > kMaxDownloadsBots) { + return; + } + auto lists = base::flat_map(); + for (auto i = 0; i != count; ++i) { + quint64 rawBotId = 0; + quint32 count = 0; + stream >> rawBotId >> count; + const auto botId = DeserializePeerId(rawBotId); + if (!botId + || !peerIsUser(botId) + || count > kMaxDownloadsPerBot + || lists.contains(botId)) { + return; + } + auto &list = lists[botId]; + list.list.reserve(count); + for (auto j = 0; j != count; ++j) { + auto entry = DownloadsEntry(); + stream >> entry.url >> entry.path >> entry.total; + entry.ready = entry.total; + entry.id = ++_autoIncrementId; + list.list.push_back(std::move(entry)); + } + } + _lists = std::move(lists); +} + +void Downloads::write() { + auto size = sizeof(quint32) // version + + sizeof(quint32); // lists count + + for (const auto &[botId, list] : _lists) { + size += sizeof(quint64) // botId + + sizeof(quint32); // list count + for (const auto &entry : list.list) { + if (entry.total > 0 && entry.ready == entry.total) { + size += Serialize::stringSize(entry.url) + + Serialize::stringSize(entry.path) + + sizeof(quint64); // total + } + } + } + + auto bytes = QByteArray(); + bytes.reserve(size); + auto buffer = QBuffer(&bytes); + buffer.open(QIODevice::WriteOnly); + auto stream = QDataStream(&buffer); + stream.setVersion(QDataStream::Qt_5_1); + + stream << quint32(kDownloadsVersion) << quint32(_lists.size()); + + for (const auto &[botId, list] : _lists) { + stream << SerializePeerId(botId) << quint32(list.list.size()); + for (const auto &entry : list.list) { + if (entry.total > 0 && entry.ready == entry.total) { + stream << entry.url << entry.path << entry.total; + } + } + } + buffer.close(); + + _session->local().writeInlineBotsDownloads(bytes); +} + +void DownloadFileBox(not_null box, DownloadBoxArgs args) { + Expects(!args.name.isEmpty()); + + box->setTitle(tr::lng_bot_download_file()); + box->addRow(object_ptr( + box, + tr::lng_bot_download_file_sure( + lt_bot, + rpl::single(Ui::Text::Bold(args.bot)), + Ui::Text::RichLangValue), + st::botDownloadLabel)); + //box->addRow(MakeFilePreview(box, args)); + const auto done = std::move(args.done); + const auto name = args.name; + const auto session = args.session; + box->addButton(tr::lng_bot_download_file_button(), [=] { + const auto path = FileNameForSave( + session, + tr::lng_save_file(tr::now), + QString(), + u"file"_q, + name, + false, + QDir()); + if (!path.isEmpty()) { + box->closeBox(); + done(path); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + done(QString()); + }); +} + +} // namespace InlineBots diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_downloads.h b/Telegram/SourceFiles/inline_bots/inline_bot_downloads.h new file mode 100644 index 000000000..33383cd7f --- /dev/null +++ b/Telegram/SourceFiles/inline_bots/inline_bot_downloads.h @@ -0,0 +1,99 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +class webFileLoader; + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class GenericBox; +} // namespace Ui + +namespace Ui::BotWebView { +struct DownloadsProgress; +} // namespace Ui::BotWebView + +namespace InlineBots { + +using DownloadId = uint32; + +using ::Ui::BotWebView::DownloadsProgress; + +struct DownloadsEntry { + DownloadId id = 0; + QString url; + QString path; + uint64 ready = 0; + uint64 total = 0; +}; + +class Downloads final { +public: + explicit Downloads(not_null session); + ~Downloads(); + + struct StartArgs { + not_null bot; + QString url; + QString path; + }; + uint32 start(StartArgs &&args); // Returns download id. + + void cancel(DownloadId id); + + [[nodiscard]] auto downloadsProgress(not_null bot) + -> rpl::producer; + +private: + struct List { + std::vector list; + bool checked = false; + }; + struct Loader { + std::unique_ptr loader; + PeerId botId = 0; + }; + + void read(); + void write(); + + void progress(PeerId botId, DownloadId id); + void fail(PeerId botId, DownloadId id); + void done(PeerId botId, DownloadId id); + void applyProgress( + PeerId botId, + DownloadId id, + int64 total, + int64 ready); + + const not_null _session; + + base::flat_map _lists; + base::flat_map _loaders; + + base::flat_map< + PeerId, + rpl::variable> _progressView; + + DownloadId _autoIncrementId = 0; + +}; + +struct DownloadBoxArgs { + not_null session; + QString bot; + QString name; + QString url; + Fn done; +}; +void DownloadFileBox(not_null box, DownloadBoxArgs args); + +} // namespace InlineBots diff --git a/Telegram/SourceFiles/storage/file_download_web.cpp b/Telegram/SourceFiles/storage/file_download_web.cpp index 33dda245f..bda894d4c 100644 --- a/Telegram/SourceFiles/storage/file_download_web.cpp +++ b/Telegram/SourceFiles/storage/file_download_web.cpp @@ -18,6 +18,7 @@ namespace { constexpr auto kMaxWebFileQueries = 8; constexpr auto kMaxHttpRedirects = 5; constexpr auto kResetDownloadPrioritiesTimeout = crl::time(200); +constexpr auto kMaxWebFile = 4000 * int64(1024 * 1024); std::weak_ptr GlobalLoadManager; @@ -41,6 +42,7 @@ enum class Error { struct Progress { qint64 ready = 0; qint64 total = 0; + QByteArray streamed; }; using Update = std::variant; @@ -67,10 +69,12 @@ private: struct Enqueued { int id = 0; QString url; + bool stream = false; }; struct Sent { QString url; not_null reply; + bool stream = false; QByteArray data; int64 ready = 0; int64 total = 0; @@ -81,7 +85,7 @@ private: void handleNetworkErrors(); // Worker thread. - void enqueue(int id, const QString &url); + void enqueue(int id, const QString &url, bool stream); void remove(int id); void resetGeneration(); void checkSendNext(); @@ -107,7 +111,11 @@ private: void failed(int id, not_null reply); void finished(int id, not_null reply); void deleteDeferred(not_null reply); - void queueProgressUpdate(int id, int64 ready, int64 total); + void queueProgressUpdate( + int id, + int64 ready, + int64 total, + QByteArray streamed); void queueFailedUpdate(int id); void queueFinishedUpdate(int id, const QByteArray &data); void clear(); @@ -187,8 +195,9 @@ void WebLoadManager::enqueue(not_null loader) { : _ids.emplace(loader, ++_autoincrement).first->second; }(); const auto url = loader->url(); + const auto stream = loader->streamLoading(); InvokeQueued(_network.get(), [=] { - enqueue(id, url); + enqueue(id, url, stream); }); } @@ -204,7 +213,7 @@ void WebLoadManager::remove(not_null loader) { }); } -void WebLoadManager::enqueue(int id, const QString &url) { +void WebLoadManager::enqueue(int id, const QString &url, bool stream) { const auto i = ranges::find(_queue, id, &Enqueued::id); if (i != end(_queue)) { return; @@ -212,7 +221,7 @@ void WebLoadManager::enqueue(int id, const QString &url) { _previousGeneration.erase( ranges::remove(_previousGeneration, id, &Enqueued::id), end(_previousGeneration)); - _queue.push_back(Enqueued{ id, url }); + _queue.push_back(Enqueued{ id, url, stream }); if (!_resetGenerationTimer.isActive()) { _resetGenerationTimer.callOnce(kResetDownloadPrioritiesTimeout); } @@ -253,7 +262,7 @@ void WebLoadManager::checkSendNext() { void WebLoadManager::send(const Enqueued &entry) { const auto id = entry.id; const auto url = entry.url; - _sent.emplace(id, Sent{ url, send(id, url) }); + _sent.emplace(id, Sent{ url, send(id, url), entry.stream }); } void WebLoadManager::removeSent(int id) { @@ -294,6 +303,13 @@ void WebLoadManager::progress( not_null reply, int64 ready, int64 total) { + if (total <= 0) { + const auto originalContentLength = reply->attribute( + QNetworkRequest::OriginalContentLengthAttribute); + if (originalContentLength.isValid()) { + total = originalContentLength.toLongLong(); + } + } const auto statusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute); const auto status = statusCode.isValid() ? statusCode.toInt() : 200; @@ -338,10 +354,7 @@ void WebLoadManager::notify( if (const auto sent = findSent(id, reply)) { sent->ready = ready; sent->total = std::max(total, int64(0)); - sent->data.append(reply->readAll()); - if (total == 0 - || total > Storage::kMaxFileInMemory - || sent->data.size() > Storage::kMaxFileInMemory) { + if (total <= 0) { LOG(("Network Error: " "Bad size received for HTTP download progress " "in WebLoadManager::onProgress(): %1 / %2 (bytes %3)" @@ -349,10 +362,43 @@ void WebLoadManager::notify( ).arg(total ).arg(sent->data.size())); failed(id, reply); - } else if (total > 0 && ready >= total) { - finished(id, reply); + return; + } + auto bytes = reply->readAll(); + if (sent->stream) { + if (total > kMaxWebFile) { + LOG(("Network Error: " + "Bad size received for HTTP download progress " + "in WebLoadManager::onProgress(): %1 / %2" + ).arg(ready + ).arg(total)); + failed(id, reply); + } else { + queueProgressUpdate( + id, + sent->ready, + sent->total, + std::move(bytes)); + if (ready >= total) { + finished(id, reply); + } + } } else { - queueProgressUpdate(id, sent->ready, sent->total); + sent->data.append(std::move(bytes)); + if (total > Storage::kMaxFileInMemory + || sent->data.size() > Storage::kMaxFileInMemory) { + LOG(("Network Error: " + "Bad size received for HTTP download progress " + "in WebLoadManager::onProgress(): %1 / %2 (bytes %3)" + ).arg(ready + ).arg(total + ).arg(sent->data.size())); + failed(id, reply); + } else if (ready >= total) { + finished(id, reply); + } else { + queueProgressUpdate(id, sent->ready, sent->total, {}); + } } } } @@ -406,9 +452,13 @@ void WebLoadManager::clear() { } } -void WebLoadManager::queueProgressUpdate(int id, int64 ready, int64 total) { - crl::on_main(this, [=] { - sendUpdate(id, Progress{ ready, total }); +void WebLoadManager::queueProgressUpdate( + int id, + int64 ready, + int64 total, + QByteArray streamed) { + crl::on_main(this, [=, bytes = std::move(streamed)]() mutable { + sendUpdate(id, Progress{ ready, total, std::move(bytes) }); }); } @@ -458,6 +508,25 @@ webFileLoader::webFileLoader( , _url(url) { } +webFileLoader::webFileLoader( + not_null session, + const QString &url, + const QString &path, + WebRequestType type) +: FileLoader( + session, + path, + 0, + 0, + UnknownFileLocation, + LoadToFileOnly, + LoadFromCloudOrLocal, + false, + 0) +, _url(url) +, _requestType(type) { +} + webFileLoader::~webFileLoader() { if (!_finished) { cancel(); @@ -468,6 +537,14 @@ QString webFileLoader::url() const { return _url; } +WebRequestType webFileLoader::requestType() const { + return _requestType; +} + +bool webFileLoader::streamLoading() const { + return (_toCache == LoadToFileOnly); +} + void webFileLoader::startLoading() { if (_finished) { return; @@ -477,7 +554,10 @@ void webFileLoader::startLoading() { this ) | rpl::start_with_next([=](const Update &data) { if (const auto progress = std::get_if(&data)) { - loadProgress(progress->ready, progress->total); + loadProgress( + progress->ready, + progress->total, + progress->streamed); } else if (const auto bytes = std::get_if(&data)) { loadFinished(*bytes); } else { @@ -492,10 +572,19 @@ int64 webFileLoader::currentOffset() const { return _ready; } -void webFileLoader::loadProgress(qint64 ready, qint64 total) { +void webFileLoader::loadProgress( + qint64 ready, + qint64 total, + const QByteArray &streamed) { _fullSize = _loadSize = total; _ready = ready; - notifyAboutProgress(); + if (!streamed.isEmpty() + && !writeResultPart(_streamedOffset, bytes::make_span(streamed))) { + loadFailed(); + } else { + _streamedOffset += streamed.size(); + notifyAboutProgress(); + } } void webFileLoader::loadFinished(const QByteArray &data) { diff --git a/Telegram/SourceFiles/storage/file_download_web.h b/Telegram/SourceFiles/storage/file_download_web.h index efa6f6b83..bc5367ca1 100644 --- a/Telegram/SourceFiles/storage/file_download_web.h +++ b/Telegram/SourceFiles/storage/file_download_web.h @@ -11,6 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class WebLoadManager; +enum class WebRequestType { + FullLoad, + OnlySize, +}; + class webFileLoader final : public FileLoader { public: webFileLoader( @@ -20,9 +25,16 @@ public: LoadFromCloudSetting fromCloud, bool autoLoading, uint8 cacheTag); + webFileLoader( + not_null session, + const QString &url, + const QString &path, + WebRequestType type); ~webFileLoader(); [[nodiscard]] QString url() const; + [[nodiscard]] WebRequestType requestType() const; + [[nodiscard]] bool streamLoading() const; int64 currentOffset() const override; @@ -33,12 +45,17 @@ private: Storage::Cache::Key cacheKey() const override; std::optional fileLocationKey() const override; - void loadProgress(qint64 ready, qint64 size); + void loadProgress( + qint64 ready, + qint64 size, + const QByteArray &streamed); void loadFinished(const QByteArray &data); void loadFailed(); const QString _url; int64 _ready = 0; + int64 _streamedOffset = 0; + WebRequestType _requestType = {}; std::shared_ptr _manager; rpl::lifetime _managerLifetime; diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index 031e61e53..3c8d81403 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -95,6 +95,7 @@ enum { // Local Storage Keys lskSearchSuggestions = 0x18, // no data lskWebviewTokens = 0x19, // data: QByteArray bots, QByteArray other lskRoundPlaceholder = 0x1a, // no data + lskInlineBotsDownloads = 0x1b, // no data }; auto EmptyMessageDraftSources() @@ -222,6 +223,7 @@ base::flat_set Account::collectGoodNames() const { _archivedCustomEmojiKey, _searchSuggestionsKey, _roundPlaceholderKey, + _inlineBotsDownloadsKey, }; auto result = base::flat_set{ "map0", @@ -309,6 +311,7 @@ Account::ReadMapResult Account::readMapWith( quint64 userSettingsKey = 0, recentHashtagsAndBotsKey = 0, exportSettingsKey = 0; quint64 searchSuggestionsKey = 0; quint64 roundPlaceholderKey = 0; + quint64 inlineBotsDownloadsKey = 0; QByteArray webviewStorageTokenBots, webviewStorageTokenOther; while (!map.stream.atEnd()) { quint32 keyType; @@ -421,6 +424,9 @@ Account::ReadMapResult Account::readMapWith( case lskRoundPlaceholder: { map.stream >> roundPlaceholderKey; } break; + case lskInlineBotsDownloads: { + map.stream >> inlineBotsDownloadsKey; + } break; case lskWebviewTokens: { map.stream >> webviewStorageTokenBots @@ -463,6 +469,7 @@ Account::ReadMapResult Account::readMapWith( _exportSettingsKey = exportSettingsKey; _searchSuggestionsKey = searchSuggestionsKey; _roundPlaceholderKey = roundPlaceholderKey; + _inlineBotsDownloadsKey = inlineBotsDownloadsKey; _oldMapVersion = mapData.version; _webviewStorageIdBots.token = webviewStorageTokenBots; _webviewStorageIdOther.token = webviewStorageTokenOther; @@ -578,6 +585,7 @@ void Account::writeMap() { + Serialize::bytearraySize(_webviewStorageIdOther.token); } if (_roundPlaceholderKey) mapSize += sizeof(quint32) + sizeof(quint64); + if (_inlineBotsDownloadsKey) mapSize += sizeof(quint32) + sizeof(quint64); EncryptedDescriptor mapData(mapSize); if (!self.isEmpty()) { @@ -652,6 +660,10 @@ void Account::writeMap() { mapData.stream << quint32(lskRoundPlaceholder); mapData.stream << quint64(_roundPlaceholderKey); } + if (_inlineBotsDownloadsKey) { + mapData.stream << quint32(lskInlineBotsDownloads); + mapData.stream << quint64(_inlineBotsDownloadsKey); + } map.writeEncrypted(mapData, _localKey); _mapChanged = false; @@ -682,6 +694,7 @@ void Account::reset() { _settingsKey = _recentHashtagsAndBotsKey = _exportSettingsKey = 0; _searchSuggestionsKey = 0; _roundPlaceholderKey = 0; + _inlineBotsDownloadsKey = 0; _oldMapVersion = 0; _fileLocations.clear(); _fileLocationPairs.clear(); @@ -3239,6 +3252,44 @@ void Account::writeRoundPlaceholder(const QImage &placeholder) { file.writeEncrypted(data, _localKey); } +QByteArray Account::readInlineBotsDownloads() { + if (_inlineBotsDownloadsRead) { + return QByteArray(); + } + _inlineBotsDownloadsRead = true; + if (!_inlineBotsDownloadsKey) { + return QByteArray(); + } + + FileReadDescriptor inlineBotsDownloads; + if (!ReadEncryptedFile( + inlineBotsDownloads, + _inlineBotsDownloadsKey, + _basePath, + _localKey)) { + ClearKey(_inlineBotsDownloadsKey, _basePath); + _inlineBotsDownloadsKey = 0; + writeMapDelayed(); + return QByteArray(); + } + + auto bytes = QByteArray(); + inlineBotsDownloads.stream >> bytes; + return bytes; +} + +void Account::writeInlineBotsDownloads(const QByteArray &bytes) { + if (!_inlineBotsDownloadsKey) { + _inlineBotsDownloadsKey = GenerateKey(_basePath); + writeMapQueued(); + } + quint32 size = Serialize::bytearraySize(bytes); + EncryptedDescriptor data(size); + data.stream << bytes; + FileWriteDescriptor file(_inlineBotsDownloadsKey, _basePath); + file.writeEncrypted(data, _localKey); +} + bool Account::encrypt( const void *src, void *dst, diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h index c0efdefdc..a044b0a74 100644 --- a/Telegram/SourceFiles/storage/storage_account.h +++ b/Telegram/SourceFiles/storage/storage_account.h @@ -177,6 +177,9 @@ public: [[nodiscard]] QImage readRoundPlaceholder(); void writeRoundPlaceholder(const QImage &placeholder); + [[nodiscard]] QByteArray readInlineBotsDownloads(); + void writeInlineBotsDownloads(const QByteArray &bytes); + [[nodiscard]] bool encrypt( const void *src, void *dst, @@ -306,6 +309,7 @@ private: FileKey _archivedCustomEmojiKey = 0; FileKey _searchSuggestionsKey = 0; FileKey _roundPlaceholderKey = 0; + FileKey _inlineBotsDownloadsKey = 0; qint64 _cacheTotalSizeLimit = 0; qint64 _cacheBigFileTotalSizeLimit = 0; @@ -317,6 +321,7 @@ private: bool _readingUserSettings = false; bool _recentHashtagsAndBotsWereRead = false; bool _searchSuggestionsRead = false; + bool _inlineBotsDownloadsRead = false; Webview::StorageId _webviewStorageIdBots; Webview::StorageId _webviewStorageIdOther; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 7158f1e9f..98e965a86 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -786,6 +786,8 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { } else { sendFullScreen(); } + } else if (command == "web_app_request_file_download") { + processDownloadRequest(arguments); } else if (command == "web_app_exit_fullscreen") { if (_fullscreen.current()) { _fullscreen = false; @@ -1008,17 +1010,17 @@ void Panel::processEmojiStatusRequest(const QJsonObject &args) { return; } const auto emojiId = args["custom_emoji_id"].toString().toULongLong(); - const auto expirationDate = TimeId(base::SafeRound( - args["expiration_date"].toDouble())); + const auto duration = TimeId(base::SafeRound( + args["duration"].toDouble())); if (!emojiId) { postEvent( "emoji_status_failed", "{ error: \"SUGGESTED_EMOJI_INVALID\" }"); return; - } else if (expirationDate < 0) { + } else if (duration < 0) { postEvent( "emoji_status_failed", - "{ error: \"EXPIRATION_DATE_INVALID\" }"); + "{ error: \"DURATION_INVALID\" }"); return; } auto callback = crl::guard(this, [=](QString error) { @@ -1032,7 +1034,7 @@ void Panel::processEmojiStatusRequest(const QJsonObject &args) { }); _delegate->botSetEmojiStatus({ .customEmojiId = emojiId, - .expirationDate = expirationDate, + .duration = duration, .callback = std::move(callback), }); } @@ -1514,6 +1516,34 @@ void Panel::processBottomBarColor(const QJsonObject &args) { } } +void Panel::processDownloadRequest(const QJsonObject &args) { + if (args.isEmpty()) { + _delegate->botClose(); + return; + } + const auto url = args["url"].toString(); + const auto name = args["file_name"].toString(); + if (url.isEmpty()) { + LOG(("BotWebView Error: Bad 'url' in download request.")); + _delegate->botClose(); + return; + } else if (name.isEmpty()) { + LOG(("BotWebView Error: Bad 'file_name' in download request.")); + _delegate->botClose(); + return; + } + const auto done = crl::guard(this, [=](bool started) { + postEvent("file_download_requested", started + ? "{ status: \"downloading\" }" + : "{ status: \"cancelled\" }"); + }); + _delegate->botDownloadFile({ + .url = url, + .name = name, + .callback = done, + }); +} + void Panel::createButton(std::unique_ptr