/* 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 "core/file_utilities.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_downloads.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 #include "base/call_delayed.h" 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() { base::take(_loaders); base::take(_lists); } 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), }); load(botId, id, list.back()); return id; } void Downloads::load( PeerId botId, DownloadId id, DownloadsEntry &entry) { entry.loading = 1; entry.failed = 0; auto &loader = _loaders[id]; Assert(!loader.loader); loader.botId = botId; loader.loader = std::make_unique( _session, entry.url, entry.path, WebRequestType::FullLoad); applyProgress(botId, id, 0, 0); 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(); } 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) { 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, bool cancel) { 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->loading = 0; k->failed = 1; if (cancel) { auto copy = *k; list.erase(k); applyProgress(botId, copy, 0, 0); } else { applyProgress(botId, *k, 0, 0); } } void Downloads::done(PeerId botId, DownloadId id) { const auto i = _loaders.find(id); if (i == end(_loaders)) { return; } const auto total = i->second.loader->fullSize(); if (total <= 0) { fail(botId, id); return; } _loaders.erase(i); auto &list = _lists[botId].list; const auto j = ranges::find( list, id, &DownloadsEntry::id); Assert(j != end(list)); j->loading = 0; 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)); applyProgress(botId, *j, total, ready); } void Downloads::applyProgress( PeerId botId, DownloadsEntry &entry, int64 total, int64 ready) { auto &progress = _progressView[botId]; auto current = progress.current(); auto subtract = int64(0); if (current.ready == current.total) { subtract = current.ready; } if (entry.total != total) { const auto delta = total - entry.total; entry.total = total; current.total += delta; } if (entry.ready != ready) { const auto delta = ready - entry.ready; entry.ready = ready; current.ready += delta; } if (subtract > 0 && current.ready >= subtract && current.total >= subtract) { current.ready -= subtract; current.total -= subtract; } if (entry.loading || current.ready < current.total) { current.loading = 1; } else { current.loading = 0; } if (total > 0 && total == ready) { write(); } progress = current; } void Downloads::action( not_null bot, DownloadId id, DownloadsAction type) { switch (type) { case DownloadsAction::Open: { const auto i = ranges::find( _lists[bot->id].list, id, &DownloadsEntry::id); if (i == end(_lists[bot->id].list)) { return; } File::ShowInFolder(i->path); } break; case DownloadsAction::Cancel: { const auto i = _loaders.find(id); if (i == end(_loaders)) { return; } const auto botId = i->second.botId; fail(botId, id, true); } break; case DownloadsAction::Retry: { const auto i = ranges::find( _lists[bot->id].list, id, &DownloadsEntry::id); if (i == end(_lists[bot->id].list)) { return; } load(bot->id, id, *i); } break; } } [[nodiscard]] auto Downloads::progress(not_null bot) ->rpl::producer { read(); return _progressView[bot->id].value(); } const std::vector &Downloads::list( not_null bot, bool forceCheck) { read(); auto &entry = _lists[bot->id]; if (forceCheck) { const auto was = int(entry.list.size()); for (auto i = begin(entry.list); i != end(entry.list);) { if (i->loading || i->failed) { ++i; } else if (auto info = QFileInfo(i->path) ; !info.exists() || info.size() != i->total) { i = entry.list.erase(i); } else { ++i; } } if (int(entry.list.size()) != was) { write(); } } return entry.list; } 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(); auto size = int64(); stream >> entry.url >> entry.path >> size; entry.total = entry.ready = size; 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); // size } } } 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; const auto chosen = std::make_shared(); 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()) { *chosen = true; box->closeBox(); done(path); } }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); box->boxClosing() | rpl::start_with_next([=] { if (!*chosen) { done(QString()); } }, box->lifetime()); } } // namespace InlineBots