Support "Device Storage" web app feature.

This commit is contained in:
John Preston 2025-04-11 13:31:16 +04:00
parent 4c2ec15f70
commit 59abfcbd6d
9 changed files with 441 additions and 5 deletions

View file

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

View file

@ -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<void(bool allowed)> callback) {
}).send();
}
bool WebViewInstance::botStorageWrite(
QString key,
std::optional<QString> value) {
return _session->attachWebView().storage().write(_bot->id, key, value);
}
std::optional<QString> 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<void(bool allowed)> 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<Main::SessionShow> WebViewInstance::uiShow() {
AttachWebView::AttachWebView(not_null<Main::Session*> session)
: _session(session)
, _downloads(std::make_unique<Downloads>(session))
, _storage(std::make_unique<Storage>(session))
, _refreshTimer([=] { requestBots(); }) {
_refreshTimer.callEach(kRefreshBotsTimeout);
}

View file

@ -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<void(bool allowed)> callback) override;
void botAllowWriteAccess(Fn<void(bool allowed)> callback) override;
bool botStorageWrite(QString key, std::optional<QString> value) override;
std::optional<QString> botStorageRead(QString key) override;
void botStorageClear() override;
void botRequestEmojiStatusAccess(
Fn<void(bool allowed)> callback) override;
void botSharePhone(Fn<void(bool shared)> 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<Main::Session*> _session;
const std::unique_ptr<Downloads> _downloads;
const std::unique_ptr<Storage> _storage;
base::Timer _refreshTimer;

View file

@ -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 <xxhash.h>
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<Main::Session*> session)
: _session(session) {
}
bool Storage::write(
PeerId botId,
const QString &key,
const std::optional<QString> &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<QString> 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

View file

@ -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<Main::Session*> session);
bool write(
PeerId botId,
const QString &key,
const std::optional<QString> &value);
std::optional<QString> read(PeerId botId, const QString &key);
void clear(PeerId botId);
private:
struct Entry {
QString key;
QString value;
};
struct List {
base::flat_map<uint64, std::vector<Entry>> 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<Main::Session*> _session;
base::flat_map<PeerId, List> _lists;
};
} // namespace InlineBots

View file

@ -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<QString> 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<PeerId, FileKey> draftsMap;
base::flat_map<PeerId, FileKey> draftCursorsMap;
base::flat_map<PeerId, bool> draftsNotReadMap;
base::flat_map<PeerId, FileKey> botStoragesMap;
base::flat_map<PeerId, bool> 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,

View file

@ -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<History*>,
base::flat_map<Data::DraftKey, MessageDraftSource>> _draftSources;
base::flat_map<PeerId, FileKey> _botStoragesMap;
base::flat_map<PeerId, bool> _botStoragesNotReadMap;
QMultiMap<MediaKey, Core::FileLocation> _fileLocations;
QMap<QString, QPair<MediaKey, Core::FileLocation>> _fileLocationPairs;

View file

@ -997,6 +997,20 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
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'."));

View file

@ -91,6 +91,12 @@ public:
QString query) = 0;
virtual void botCheckWriteAccess(Fn<void(bool allowed)> callback) = 0;
virtual void botAllowWriteAccess(Fn<void(bool allowed)> callback) = 0;
virtual bool botStorageWrite(
QString key,
std::optional<QString> value) = 0;
[[nodiscard]] virtual std::optional<QString> botStorageRead(
QString key) = 0;
virtual void botStorageClear() = 0;
virtual void botRequestEmojiStatusAccess(
Fn<void(bool allowed)> callback) = 0;
virtual void botSharePhone(Fn<void(bool shared)> 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<Button> &button,
const QJsonObject &args);
@ -188,6 +197,12 @@ private:
void replyCustomMethod(QJsonValue requestId, QJsonObject response);
void requestClipboardText(const QJsonObject &args);
void setupClosingBehaviour(const QJsonObject &args);
void replyDeviceStorage(
const QJsonObject &args,
const QString &event,
QJsonObject response);
void deviceStorageFailed(const QJsonObject &args, QString error);
void secureStorageFailed(const QJsonObject &args);
void createButton(std::unique_ptr<Button> &button);
void scheduleCloseWithConfirmation();
void closeWithConfirmation();