From 59abfcbd6db46b9e552231c902183d41aee2c08e Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 11 Apr 2025 13:31:16 +0400 Subject: [PATCH] Support "Device Storage" web app feature. --- Telegram/CMakeLists.txt | 2 + .../inline_bots/bot_attach_web_view.cpp | 26 ++- .../inline_bots/bot_attach_web_view.h | 8 + .../inline_bots/inline_bot_storage.cpp | 181 ++++++++++++++++++ .../inline_bots/inline_bot_storage.h | 52 +++++ .../SourceFiles/storage/storage_account.cpp | 86 +++++++++ .../SourceFiles/storage/storage_account.h | 5 + .../ui/chat/attach/attach_bot_webview.cpp | 71 +++++++ .../ui/chat/attach/attach_bot_webview.h | 15 ++ 9 files changed, 441 insertions(+), 5 deletions(-) create mode 100644 Telegram/SourceFiles/inline_bots/inline_bot_storage.cpp create mode 100644 Telegram/SourceFiles/inline_bots/inline_bot_storage.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 7714f518d8..bc0d1b9016 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1083,6 +1083,8 @@ PRIVATE inline_bots/inline_bot_result.h inline_bots/inline_bot_send_data.cpp inline_bots/inline_bot_send_data.h + inline_bots/inline_bot_storage.cpp + inline_bots/inline_bot_storage.h inline_bots/inline_results_inner.cpp inline_bots/inline_results_inner.h inline_bots/inline_results_widget.cpp diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index d7fad92224..92664d402e 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -48,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "inline_bots/inline_bot_result.h" #include "inline_bots/inline_bot_confirm_prepared.h" #include "inline_bots/inline_bot_downloads.h" +#include "inline_bots/inline_bot_storage.h" #include "iv/iv_instance.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" @@ -1502,7 +1503,7 @@ void WebViewInstance::botHandleInvoice(QString slug) { } }; Payments::CheckoutProcess::Start( - &_bot->session(), + _session, slug, reactivate, nonPanelPaymentFormFactory(reactivate)); @@ -1698,6 +1699,20 @@ void WebViewInstance::botAllowWriteAccess(Fn callback) { }).send(); } +bool WebViewInstance::botStorageWrite( + QString key, + std::optional value) { + return _session->attachWebView().storage().write(_bot->id, key, value); +} + +std::optional WebViewInstance::botStorageRead(QString key) { + return _session->attachWebView().storage().read(_bot->id, key); +} + +void WebViewInstance::botStorageClear() { + _session->attachWebView().storage().clear(_bot->id); +} + void WebViewInstance::botRequestEmojiStatusAccess( Fn callback) { if (_bot->botInfo->canManageEmojiStatus) { @@ -1955,20 +1970,20 @@ void WebViewInstance::botDownloadFile( callback(false); return; } - _bot->session().attachWebView().downloads().start({ + _session->attachWebView().downloads().start({ .bot = _bot, .url = request.url, .path = path, }); callback(true); }; - _bot->session().api().request(MTPbots_CheckDownloadFileParams( + _session->api().request(MTPbots_CheckDownloadFileParams( _bot->inputUser, MTP_string(request.name), MTP_string(request.url) )).done([=] { _panel->showBox(Box(DownloadFileBox, DownloadBoxArgs{ - .session = &_bot->session(), + .session = _session, .bot = _bot->name(), .name = base::FileNameFromUserString(request.name), .url = request.url, @@ -2018,7 +2033,7 @@ void WebViewInstance::botOpenPrivacyPolicy() { }; const auto openUrl = [=](const QString &url) { Core::App().iv().openWithIvPreferred( - &_bot->session(), + _session, url, makeOtherContext(false)); }; @@ -2092,6 +2107,7 @@ std::shared_ptr WebViewInstance::uiShow() { AttachWebView::AttachWebView(not_null session) : _session(session) , _downloads(std::make_unique(session)) +, _storage(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 43dc766890..442cc3fc78 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -53,6 +53,7 @@ namespace InlineBots { class WebViewInstance; class Downloads; +class Storage; enum class PeerType : uint8 { SameBot = 0x01, @@ -276,6 +277,9 @@ private: QString query) override; void botCheckWriteAccess(Fn callback) override; void botAllowWriteAccess(Fn callback) override; + bool botStorageWrite(QString key, std::optional value) override; + std::optional botStorageRead(QString key) override; + void botStorageClear() override; void botRequestEmojiStatusAccess( Fn callback) override; void botSharePhone(Fn callback) override; @@ -322,6 +326,9 @@ public: [[nodiscard]] Downloads &downloads() const { return *_downloads; } + [[nodiscard]] Storage &storage() const { + return *_storage; + } void open(WebViewDescriptor &&descriptor); void openByUsername( @@ -394,6 +401,7 @@ private: const not_null _session; const std::unique_ptr _downloads; + const std::unique_ptr _storage; base::Timer _refreshTimer; diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_storage.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_storage.cpp new file mode 100644 index 0000000000..8d753de2e9 --- /dev/null +++ b/Telegram/SourceFiles/inline_bots/inline_bot_storage.cpp @@ -0,0 +1,181 @@ +/* +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_storage.h" + +#include "main/main_session.h" +#include "storage/storage_account.h" + +#include + +namespace InlineBots { +namespace { + +constexpr auto kMaxStorageSize = (5 << 20); + +[[nodiscard]] uint64 KeyHash(const QString &key) { + return XXH64(key.data(), key.size(), 0); +} + +} // namespace + +Storage::Storage(not_null session) +: _session(session) { +} + +bool Storage::write( + PeerId botId, + const QString &key, + const std::optional &value) { + if (value && value->size() > kMaxStorageSize) { + return false; + } + readFromDisk(botId); + auto i = _lists.find(botId); + if (i == end(_lists)) { + if (!value) { + return true; + } + i = _lists.emplace(botId).first; + } + auto &list = i->second; + const auto hash = KeyHash(key); + auto j = list.data.find(hash); + if (j == end(list.data)) { + if (!value) { + return true; + } + j = list.data.emplace(hash).first; + } + auto &bykey = j->second; + const auto k = ranges::find(bykey, key, &Entry::key); + if (k == end(bykey) && !value) { + return true; + } + const auto size = list.totalSize + - (k != end(bykey) ? (key.size() + k->value.size()) : 0) + + (value ? (key.size() + value->size()) : 0); + if (size > kMaxStorageSize) { + return false; + } + if (k == end(bykey)) { + bykey.emplace_back(key, *value); + ++list.keysCount; + } else if (value) { + k->value = *value; + } else { + bykey.erase(k); + --list.keysCount; + } + if (bykey.empty()) { + list.data.erase(j); + if (list.data.empty()) { + Assert(size == 0); + _lists.erase(i); + } else { + list.totalSize = size; + } + } else { + list.totalSize = size; + } + saveToDisk(botId); + return true; +} + +std::optional Storage::read(PeerId botId, const QString &key) { + readFromDisk(botId); + const auto i = _lists.find(botId); + if (i == end(_lists)) { + return std::nullopt; + } + const auto &list = i->second; + const auto j = list.data.find(KeyHash(key)); + if (j == end(list.data)) { + return std::nullopt; + } + const auto &bykey = j->second; + const auto k = ranges::find(bykey, key, &Entry::key); + if (k == end(bykey)) { + return std::nullopt; + } + return k->value; +} + +void Storage::clear(PeerId botId) { + if (_lists.remove(botId)) { + saveToDisk(botId); + } +} + +void Storage::saveToDisk(PeerId botId) { + const auto i = _lists.find(botId); + if (i != end(_lists)) { + _session->local().writeBotStorage(botId, Serialize(i->second)); + } else { + _session->local().writeBotStorage(botId, QByteArray()); + } +} + +void Storage::readFromDisk(PeerId botId) { + const auto serialized = _session->local().readBotStorage(botId); + if (!serialized.isEmpty()) { + _lists[botId] = Deserialize(serialized); + } +} + +QByteArray Storage::Serialize(const List &list) { + auto result = QByteArray(); + const auto size = sizeof(quint32) + + (list.keysCount * sizeof(quint32)) + + (list.totalSize * sizeof(ushort)); + result.reserve(size); + { + QDataStream stream(&result, QIODevice::WriteOnly); + auto count = 0; + stream.setVersion(QDataStream::Qt_5_1); + stream << quint32(list.keysCount); + for (const auto &[hash, bykey] : list.data) { + for (const auto &entry : bykey) { + stream << entry.key << entry.value; + ++count; + } + } + Assert(count == list.keysCount); + } + return result; +} + +Storage::List Storage::Deserialize(const QByteArray &serialized) { + QDataStream stream(serialized); + stream.setVersion(QDataStream::Qt_5_1); + + auto count = quint32(); + auto result = List(); + stream >> count; + if (count > kMaxStorageSize) { + return {}; + } + for (auto i = 0; i != count; ++i) { + auto entry = Entry(); + stream >> entry.key >> entry.value; + const auto hash = KeyHash(entry.key); + auto j = result.data.find(hash); + if (j == end(result.data)) { + j = result.data.emplace(hash).first; + } + auto &bykey = j->second; + const auto k = ranges::find(bykey, entry.key, &Entry::key); + if (k == end(bykey)) { + bykey.push_back(entry); + result.totalSize += entry.key.size() + entry.value.size(); + ++result.keysCount; + } + } + return result; +} + +} // namespace InlineBots diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_storage.h b/Telegram/SourceFiles/inline_bots/inline_bot_storage.h new file mode 100644 index 0000000000..975fa576b5 --- /dev/null +++ b/Telegram/SourceFiles/inline_bots/inline_bot_storage.h @@ -0,0 +1,52 @@ +/* +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 + +#include "ui/chat/attach/attach_bot_webview.h" + +namespace Main { +class Session; +} // namespace Main + +namespace InlineBots { + +class Storage final { +public: + explicit Storage(not_null session); + + bool write( + PeerId botId, + const QString &key, + const std::optional &value); + std::optional read(PeerId botId, const QString &key); + void clear(PeerId botId); + +private: + struct Entry { + QString key; + QString value; + }; + struct List { + base::flat_map> data; + int keysCount = 0; + int totalSize = 0; + }; + + void saveToDisk(PeerId botId); + void readFromDisk(PeerId botId); + + [[nodiscard]] static QByteArray Serialize(const List &list); + [[nodiscard]] static List Deserialize(const QByteArray &serialized); + + const not_null _session; + + base::flat_map _lists; + +}; + +} // namespace InlineBots diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index ea673e5039..339840de23 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -98,6 +98,7 @@ enum { // Local Storage Keys lskRoundPlaceholder = 0x1a, // no data lskInlineBotsDownloads = 0x1b, // no data lskMediaLastPlaybackPositions = 0x1c, // no data + lskBotStorages = 0x1d, // data: PeerId botId }; auto EmptyMessageDraftSources() @@ -255,6 +256,9 @@ base::flat_set Account::collectGoodNames() const { for (const auto &[key, value] : _draftCursorsMap) { push(value); } + for (const auto &[key, value] : _botStoragesMap) { + push(value); + } for (const auto &value : keys) { push(value); } @@ -308,6 +312,8 @@ Account::ReadMapResult Account::readMapWith( base::flat_map draftsMap; base::flat_map draftCursorsMap; base::flat_map draftsNotReadMap; + base::flat_map botStoragesMap; + base::flat_map botStoragesNotReadMap; quint64 locationsKey = 0, reportSpamStatusesKey = 0, trustedPeersKey = 0; quint64 recentStickersKeyOld = 0; quint64 installedStickersKey = 0, featuredStickersKey = 0, recentStickersKey = 0, favedStickersKey = 0, archivedStickersKey = 0; @@ -443,6 +449,18 @@ Account::ReadMapResult Account::readMapWith( >> webviewStorageTokenBots >> webviewStorageTokenOther; } break; + case lskBotStorages: { + quint32 count = 0; + map.stream >> count; + for (quint32 i = 0; i < count; ++i) { + FileKey key; + quint64 peerIdSerialized; + map.stream >> key >> peerIdSerialized; + const auto peerId = DeserializePeerId(peerIdSerialized); + botStoragesMap.emplace(peerId, key); + botStoragesNotReadMap.emplace(peerId, true); + } + } break; default: LOG(("App Error: unknown key type in encrypted map: %1").arg(keyType)); return ReadMapResult::Failed; @@ -457,6 +475,8 @@ Account::ReadMapResult Account::readMapWith( _draftsMap = draftsMap; _draftCursorsMap = draftCursorsMap; _draftsNotReadMap = draftsNotReadMap; + _botStoragesMap = botStoragesMap; + _botStoragesNotReadMap = botStoragesNotReadMap; _locationsKey = locationsKey; _trustedPeersKey = trustedPeersKey; @@ -599,6 +619,7 @@ void Account::writeMap() { if (_roundPlaceholderKey) mapSize += sizeof(quint32) + sizeof(quint64); if (_inlineBotsDownloadsKey) mapSize += sizeof(quint32) + sizeof(quint64); if (_mediaLastPlaybackPositionsKey) mapSize += sizeof(quint32) + sizeof(quint64); + if (!_botStoragesMap.empty()) mapSize += sizeof(quint32) * 2 + _botStoragesMap.size() * sizeof(quint64) * 2; EncryptedDescriptor mapData(mapSize); if (!self.isEmpty()) { @@ -681,6 +702,12 @@ void Account::writeMap() { mapData.stream << quint32(lskMediaLastPlaybackPositions); mapData.stream << quint64(_mediaLastPlaybackPositionsKey); } + if (!_botStoragesMap.empty()) { + mapData.stream << quint32(lskBotStorages) << quint32(_botStoragesMap.size()); + for (const auto &[key, value] : _botStoragesMap) { + mapData.stream << quint64(value) << SerializePeerId(key); + } + } map.writeEncrypted(mapData, _localKey); _mapChanged = false; @@ -693,6 +720,8 @@ void Account::reset() { _draftsMap.clear(); _draftCursorsMap.clear(); _draftsNotReadMap.clear(); + _botStoragesMap.clear(); + _botStoragesNotReadMap.clear(); _locationsKey = _trustedPeersKey = 0; _recentStickersKeyOld = 0; _installedStickersKey = 0; @@ -3473,6 +3502,63 @@ void Account::writeInlineBotsDownloads(const QByteArray &bytes) { file.writeEncrypted(data, _localKey); } +void Account::writeBotStorage(PeerId botId, const QByteArray &serialized) { + if (serialized.isEmpty()) { + auto i = _botStoragesMap.find(botId); + if (i != _botStoragesMap.cend()) { + ClearKey(i->second, _basePath); + _botStoragesMap.erase(i); + writeMapDelayed(); + } + _botStoragesNotReadMap.remove(botId); + return; + } + + auto i = _botStoragesMap.find(botId); + if (i == _botStoragesMap.cend()) { + i = _botStoragesMap.emplace(botId, GenerateKey(_basePath)).first; + writeMapQueued(); + } + + auto size = Serialize::bytearraySize(serialized); + + EncryptedDescriptor data(size); + data.stream << serialized; + + FileWriteDescriptor file(i->second, _basePath); + file.writeEncrypted(data, _localKey); + + _botStoragesNotReadMap.remove(botId); +} + +QByteArray Account::readBotStorage(PeerId botId) { + if (!_botStoragesNotReadMap.remove(botId)) { + return {}; + } + + const auto j = _botStoragesMap.find(botId); + if (j == _botStoragesMap.cend()) { + return {}; + } + FileReadDescriptor storage; + if (!ReadEncryptedFile(storage, j->second, _basePath, _localKey)) { + ClearKey(j->second, _basePath); + _botStoragesMap.erase(j); + writeMapDelayed(); + return {}; + } + + auto result = QByteArray(); + storage.stream >> result; + if (storage.stream.status() != QDataStream::Ok) { + ClearKey(j->second, _basePath); + _botStoragesMap.erase(j); + writeMapDelayed(); + return {}; + } + return result; +} + 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 86bdabde6f..c096039941 100644 --- a/Telegram/SourceFiles/storage/storage_account.h +++ b/Telegram/SourceFiles/storage/storage_account.h @@ -191,6 +191,9 @@ public: [[nodiscard]] QByteArray readInlineBotsDownloads(); void writeInlineBotsDownloads(const QByteArray &bytes); + void writeBotStorage(PeerId botId, const QByteArray &serialized); + [[nodiscard]] QByteArray readBotStorage(PeerId botId); + [[nodiscard]] bool encrypt( const void *src, void *dst, @@ -293,6 +296,8 @@ private: base::flat_map< not_null, base::flat_map> _draftSources; + base::flat_map _botStoragesMap; + base::flat_map _botStoragesNotReadMap; QMultiMap _fileLocations; QMap> _fileLocationPairs; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index df4d7bc17a..526dceceed 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -997,6 +997,20 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { processEmojiStatusRequest(arguments); } else if (command == "web_app_request_emoji_status_access") { processEmojiStatusAccessRequest(); + } else if (command == "web_app_device_storage_save_key") { + processStorageSaveKey(arguments); + } else if (command == "web_app_device_storage_get_key") { + processStorageGetKey(arguments); + } else if (command == "web_app_device_storage_clear") { + processStorageClear(arguments); + } else if (command == "web_app_secure_storage_save_key") { + secureStorageFailed(arguments); + } else if (command == "web_app_secure_storage_get_key") { + secureStorageFailed(arguments); + } else if (command == "web_app_secure_storage_restore_key") { + secureStorageFailed(arguments); + } else if (command == "web_app_secure_storage_clear") { + secureStorageFailed(arguments); } else if (command == "share_score") { _delegate->botHandleMenuButton(MenuButton::ShareGame); } @@ -1201,6 +1215,63 @@ void Panel::processEmojiStatusAccessRequest() { _delegate->botRequestEmojiStatusAccess(std::move(callback)); } +void Panel::processStorageSaveKey(const QJsonObject &args) { + const auto keyObject = args["key"]; + const auto valueObject = args["value"]; + const auto key = keyObject.toString(); + if (!keyObject.isString()) { + deviceStorageFailed(args, u"KEY_INVALID"_q); + } else if (valueObject.isNull()) { + _delegate->botStorageWrite(key, std::nullopt); + replyDeviceStorage(args, u"device_storage_key_saved"_q, {}); + } else if (!valueObject.isString()) { + deviceStorageFailed(args, u"VALUE_INVALID"_q); + } else if (_delegate->botStorageWrite(key, valueObject.toString())) { + replyDeviceStorage(args, u"device_storage_key_saved"_q, {}); + } else { + deviceStorageFailed(args, u"QUOTA_EXCEEDED"_q); + } +} + +void Panel::processStorageGetKey(const QJsonObject &args) { + const auto keyObject = args["key"]; + const auto key = keyObject.toString(); + if (!keyObject.isString()) { + deviceStorageFailed(args, u"KEY_INVALID"_q); + } else { + const auto value = _delegate->botStorageRead(key); + replyDeviceStorage(args, u"device_storage_key_received"_q, { + { u"value"_q, value ? QJsonValue(*value) : QJsonValue::Null }, + }); + } +} + +void Panel::processStorageClear(const QJsonObject &args) { + _delegate->botStorageClear(); + replyDeviceStorage(args, u"device_storage_cleared"_q, {}); +} + +void Panel::replyDeviceStorage( + const QJsonObject &args, + const QString &event, + QJsonObject response) { + response[u"req_id"_q] = args[u"req_id"_q]; + postEvent(event, response); +} + +void Panel::deviceStorageFailed(const QJsonObject &args, QString error) { + replyDeviceStorage(args, u"device_storage_failed"_q, { + { u"error"_q, error }, + }); +} + +void Panel::secureStorageFailed(const QJsonObject &args) { + postEvent(u"secure_storage_failed"_q, QJsonObject{ + { u"req_id"_q, args["req_id"] }, + { u"error"_q, u"UNSUPPORTED"_q }, + }); +} + void Panel::openTgLink(const QJsonObject &args) { if (args.isEmpty()) { LOG(("BotWebView Error: Bad arguments in 'web_app_open_tg_link'.")); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index b17a648a49..72edb0b391 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -91,6 +91,12 @@ public: QString query) = 0; virtual void botCheckWriteAccess(Fn callback) = 0; virtual void botAllowWriteAccess(Fn callback) = 0; + virtual bool botStorageWrite( + QString key, + std::optional value) = 0; + [[nodiscard]] virtual std::optional botStorageRead( + QString key) = 0; + virtual void botStorageClear() = 0; virtual void botRequestEmojiStatusAccess( Fn callback) = 0; virtual void botSharePhone(Fn callback) = 0; @@ -165,6 +171,9 @@ private: void processSendMessageRequest(const QJsonObject &args); void processEmojiStatusRequest(const QJsonObject &args); void processEmojiStatusAccessRequest(); + void processStorageSaveKey(const QJsonObject &args); + void processStorageGetKey(const QJsonObject &args); + void processStorageClear(const QJsonObject &args); void processButtonMessage( std::unique_ptr