Implement download of files in miniapps.

This commit is contained in:
John Preston 2024-11-12 21:32:21 +04:00
parent 2fed657940
commit 341ab781b2
14 changed files with 721 additions and 31 deletions

View file

@ -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

View file

@ -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";

View file

@ -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<Ui::GenericBox*> box,
not_null<UserData*> bot,
not_null<DocumentData*> document,
TimeId until,
TimeId duration,
Fn<void(bool)> done) {
box->setNoContentMargin(true);
@ -608,7 +610,9 @@ void ConfirmEmojiStatusBox(
object_ptr<Ui::RpWidget>::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<WebViewSourceAttachMenu>(_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<Main::SessionShow> WebViewInstance::uiShow() {
AttachWebView::AttachWebView(not_null<Main::Session*> session)
: _session(session)
, _downloads(std::make_unique<Downloads>(session))
, _refreshTimer([=] { requestBots(); }) {
_refreshTimer.callEach(kRefreshBotsTimeout);
}

View file

@ -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<Main::Session*> session);
~AttachWebView();
[[nodiscard]] Downloads &downloads() const {
return *_downloads;
}
void open(WebViewDescriptor &&descriptor);
void openByUsername(
not_null<Window::SessionController*> controller,
@ -370,6 +378,7 @@ private:
Fn<void(bool added)> callback = nullptr);
const not_null<Main::Session*> _session;
const std::unique_ptr<Downloads> _downloads;
base::Timer _refreshTimer;

View file

@ -11,6 +11,10 @@ namespace Data {
class Thread;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class GenericBox;
} // namespace Ui

View file

@ -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 <QtCore/QBuffer>
#include <QtCore/QDataStream>
namespace InlineBots {
namespace {
constexpr auto kDownloadsVersion = 1;
constexpr auto kMaxDownloadsBots = 4096;
constexpr auto kMaxDownloadsPerBot = 16384;
} // namespace
Downloads::Downloads(not_null<Main::Session*> 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<webFileLoader>(
_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<UserData*> bot)
->rpl::producer<DownloadsProgress> {
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<PeerId, List>();
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<Ui::GenericBox*> box, DownloadBoxArgs args) {
Expects(!args.name.isEmpty());
box->setTitle(tr::lng_bot_download_file());
box->addRow(object_ptr<Ui::FlatLabel>(
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

View file

@ -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<Main::Session*> session);
~Downloads();
struct StartArgs {
not_null<UserData*> bot;
QString url;
QString path;
};
uint32 start(StartArgs &&args); // Returns download id.
void cancel(DownloadId id);
[[nodiscard]] auto downloadsProgress(not_null<UserData*> bot)
-> rpl::producer<DownloadsProgress>;
private:
struct List {
std::vector<DownloadsEntry> list;
bool checked = false;
};
struct Loader {
std::unique_ptr<webFileLoader> 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<Main::Session*> _session;
base::flat_map<PeerId, List> _lists;
base::flat_map<DownloadId, Loader> _loaders;
base::flat_map<
PeerId,
rpl::variable<DownloadsProgress>> _progressView;
DownloadId _autoIncrementId = 0;
};
struct DownloadBoxArgs {
not_null<Main::Session*> session;
QString bot;
QString name;
QString url;
Fn<void(QString)> done;
};
void DownloadFileBox(not_null<Ui::GenericBox*> box, DownloadBoxArgs args);
} // namespace InlineBots

View file

@ -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<WebLoadManager> GlobalLoadManager;
@ -41,6 +42,7 @@ enum class Error {
struct Progress {
qint64 ready = 0;
qint64 total = 0;
QByteArray streamed;
};
using Update = std::variant<Progress, QByteArray, Error>;
@ -67,10 +69,12 @@ private:
struct Enqueued {
int id = 0;
QString url;
bool stream = false;
};
struct Sent {
QString url;
not_null<QNetworkReply*> 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<QNetworkReply*> reply);
void finished(int id, not_null<QNetworkReply*> reply);
void deleteDeferred(not_null<QNetworkReply*> 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<webFileLoader*> 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<webFileLoader*> 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<QNetworkReply*> 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<Main::Session*> 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<Progress>(&data)) {
loadProgress(progress->ready, progress->total);
loadProgress(
progress->ready,
progress->total,
progress->streamed);
} else if (const auto bytes = std::get_if<QByteArray>(&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) {

View file

@ -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<Main::Session*> 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<MediaKey> 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<WebLoadManager> _manager;
rpl::lifetime _managerLifetime;

View file

@ -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<QString> Account::collectGoodNames() const {
_archivedCustomEmojiKey,
_searchSuggestionsKey,
_roundPlaceholderKey,
_inlineBotsDownloadsKey,
};
auto result = base::flat_set<QString>{
"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,

View file

@ -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;

View file

@ -786,6 +786,8 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
} 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<Button> &button) {
if (!_bottomButtonsBg) {
_bottomButtonsBg = std::make_unique<RpWidget>(_widget.get());

View file

@ -53,10 +53,25 @@ struct CustomMethodRequest {
struct SetEmojiStatusRequest {
uint64 customEmojiId = 0;
TimeId expirationDate = 0;
TimeId duration = 0;
Fn<void(QString)> callback;
};
struct DownloadFileRequest {
QString url;
QString name;
Fn<void(bool)> callback;
};
struct DownloadsProgress {
uint64 ready = 0;
uint64 total = 0;
friend inline bool operator==(
const DownloadsProgress &a,
const DownloadsProgress &b) = default;
};
struct SendPreparedMessageRequest {
QString id = 0;
Fn<void(QString)> callback;
@ -81,6 +96,7 @@ public:
virtual void botSharePhone(Fn<void(bool shared)> callback) = 0;
virtual void botInvokeCustomMethod(CustomMethodRequest request) = 0;
virtual void botSetEmojiStatus(SetEmojiStatusRequest request) = 0;
virtual void botDownloadFile(DownloadFileRequest request) = 0;
virtual void botSendPreparedMessage(
SendPreparedMessageRequest request) = 0;
virtual void botOpenPrivacyPolicy() = 0;
@ -97,6 +113,7 @@ struct Args {
MenuButtons menuButtons;
bool fullscreen = false;
bool allowClipboardRead = false;
rpl::producer<DownloadsProgress> downloadsProgress;
};
class Panel final : public base::has_weak_ptr {
@ -155,6 +172,7 @@ private:
void processHeaderColor(const QJsonObject &args);
void processBackgroundColor(const QJsonObject &args);
void processBottomBarColor(const QJsonObject &args);
void processDownloadRequest(const QJsonObject &args);
void openTgLink(const QJsonObject &args);
void openExternalLink(const QJsonObject &args);
void openInvoice(const QJsonObject &args);

View file

@ -1181,3 +1181,5 @@ botEmojiStatusName: FlatLabel(defaultFlatLabel) {
botEmojiStatusEmoji: FlatLabel(botEmojiStatusName) {
textFg: profileVerifiedCheckBg;
}
botDownloadLabel: boxLabel;