diff --git a/Telegram/SourceFiles/core/core_settings_proxy.cpp b/Telegram/SourceFiles/core/core_settings_proxy.cpp index b219f4fca..1807aa2ef 100644 --- a/Telegram/SourceFiles/core/core_settings_proxy.cpp +++ b/Telegram/SourceFiles/core/core_settings_proxy.cpp @@ -95,9 +95,6 @@ SettingsProxy::SettingsProxy() } QByteArray SettingsProxy::serialize() const { - auto result = QByteArray(); - auto stream = QDataStream(&result, QIODevice::WriteOnly); - const auto serializedSelected = SerializeProxyData(_selected); const auto serializedList = ranges::views::all( _list @@ -111,9 +108,7 @@ QByteArray SettingsProxy::serialize() const { 0, ranges::plus(), &Serialize::bytearraySize); - result.reserve(size); - - stream.setVersion(QDataStream::Qt_5_1); + auto stream = Serialize::ByteArrayWriter(size); stream << qint32(_tryIPv6 ? 1 : 0) << qint32(_useProxyForCalls ? 1 : 0) @@ -123,9 +118,7 @@ QByteArray SettingsProxy::serialize() const { for (const auto &i : serializedList) { stream << i; } - - stream.device()->close(); - return result; + return std::move(stream).result(); } bool SettingsProxy::setFromSerialized(const QByteArray &serialized) { @@ -133,7 +126,7 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) { return true; } - auto stream = QDataStream(serialized); + auto stream = Serialize::ByteArrayReader(serialized); auto tryIPv6 = qint32(_tryIPv6 ? 1 : 0); auto useProxyForCalls = qint32(_useProxyForCalls ? 1 : 0); @@ -148,7 +141,7 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) { >> settings >> selectedProxy >> listCount; - if (stream.status() == QDataStream::Ok) { + if (stream.ok()) { for (auto i = 0; i != listCount; ++i) { QByteArray data; stream >> data; @@ -157,7 +150,7 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) { } } - if (stream.status() != QDataStream::Ok) { + if (!stream.ok()) { LOG(("App Error: " "Bad data for Core::SettingsProxy::setFromSerialized()")); return false; diff --git a/Telegram/SourceFiles/data/components/top_peers.cpp b/Telegram/SourceFiles/data/components/top_peers.cpp index 8e6149259..8a06b427b 100644 --- a/Telegram/SourceFiles/data/components/top_peers.cpp +++ b/Telegram/SourceFiles/data/components/top_peers.cpp @@ -14,6 +14,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "main/main_session.h" #include "mtproto/mtproto_config.h" +#include "storage/serialize_common.h" +#include "storage/serialize_peer.h" +#include "storage/storage_account.h" namespace Data { namespace { @@ -25,6 +28,19 @@ constexpr auto kRequestTimeLimit = 10 * crl::time(1000); return std::exp((now - was) * 1. / decay); } +[[nodiscard]] quint64 SerializeRating(float64 rating) { + return quint64( + base::SafeRound(std::clamp(rating, 0., 1'000'000.) * 1'000'000.)); +} + +[[nodiscard]] float64 DeserializeRating(quint64 rating) { + return std::clamp( + rating, + quint64(), + quint64(1'000'000'000'000ULL) + ) / 1'000'000.; +} + } // namespace TopPeers::TopPeers(not_null session) @@ -43,12 +59,16 @@ TopPeers::TopPeers(not_null session) TopPeers::~TopPeers() = default; std::vector> TopPeers::list() const { + _session->local().readSearchSuggestions(); + return _list | ranges::view::transform(&TopPeer::peer) | ranges::to_vector; } bool TopPeers::disabled() const { + _session->local().readSearchSuggestions(); + return _disabled; } @@ -67,9 +87,13 @@ void TopPeers::remove(not_null peer) { MTP_topPeerCategoryCorrespondents(), peer->input )).send(); + + _session->local().writeSearchSuggestionsDelayed(); } void TopPeers::increment(not_null peer, TimeId date) { + _session->local().readSearchSuggestions(); + if (_disabled || date <= _lastReceivedDate) { return; } @@ -95,6 +119,8 @@ void TopPeers::increment(not_null peer, TimeId date) { if (changed) { _updates.fire({}); } + + _session->local().writeSearchSuggestionsDelayed(); } } @@ -108,6 +134,8 @@ void TopPeers::reload() { } void TopPeers::toggleDisabled(bool disabled) { + _session->local().readSearchSuggestions(); + if (disabled) { if (!_disabled || !_list.empty()) { _disabled = true; @@ -126,6 +154,8 @@ void TopPeers::toggleDisabled(bool disabled) { request(); } }).send(); + + _session->local().writeSearchSuggestionsDelayed(); } void TopPeers::request() { @@ -182,10 +212,67 @@ void TopPeers::request() { uint64 TopPeers::countHash() const { using namespace Api; auto hash = HashInit(); - for (const auto &top : _list) { + for (const auto &top : _list | ranges::views::take(kLimit)) { HashUpdate(hash, peerToUser(top.peer->id).bare); } return HashFinalize(hash); } +QByteArray TopPeers::serialize() const { + _session->local().readSearchSuggestions(); + + if (!_disabled && _list.empty()) { + return {}; + } + auto size = 3 * sizeof(quint32); // AppVersion, disabled, count + const auto count = std::min(int(_list.size()), kLimit); + auto &&list = _list | ranges::views::take(count); + for (const auto &top : list) { + size += Serialize::peerSize(top.peer) + sizeof(quint64); + } + auto stream = Serialize::ByteArrayWriter(size); + stream + << quint32(AppVersion) + << quint32(_disabled ? 1 : 0) + << quint32(_list.size()); + for (const auto &top : list) { + Serialize::writePeer(stream, top.peer); + stream << SerializeRating(top.rating); + } + return std::move(stream).result(); +} + +void TopPeers::applyLocal(QByteArray serialized) { + if (_lastReceived || serialized.isEmpty()) { + return; + } + auto stream = Serialize::ByteArrayReader(serialized); + auto streamAppVersion = quint32(); + auto disabled = quint32(); + auto count = quint32(); + stream >> streamAppVersion >> disabled >> count; + if (!stream.ok()) { + return; + } + _list.reserve(count); + for (auto i = 0; i != int(count); ++i) { + auto rating = quint64(); + const auto peer = Serialize::readPeer( + _session, + streamAppVersion, + stream); + stream >> rating; + if (stream.ok() && peer) { + _list.push_back({ + .peer = peer, + .rating = DeserializeRating(rating), + }); + } else { + _list.clear(); + return; + } + } + _disabled = (disabled == 1); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/components/top_peers.h b/Telegram/SourceFiles/data/components/top_peers.h index da44a0c6c..051964066 100644 --- a/Telegram/SourceFiles/data/components/top_peers.h +++ b/Telegram/SourceFiles/data/components/top_peers.h @@ -27,6 +27,9 @@ public: void reload(); void toggleDisabled(bool disabled); + [[nodiscard]] QByteArray serialize() const; + void applyLocal(QByteArray serialized); + private: struct TopPeer { not_null peer; diff --git a/Telegram/SourceFiles/main/main_account.cpp b/Telegram/SourceFiles/main/main_account.cpp index 90a6a897e..9e366ca54 100644 --- a/Telegram/SourceFiles/main/main_account.cpp +++ b/Telegram/SourceFiles/main/main_account.cpp @@ -53,6 +53,7 @@ Account::Account(not_null domain, const QString &dataName, int index) Account::~Account() { if (const auto session = maybeSession()) { session->saveSettingsNowIfNeeded(); + _local->writeSearchSuggestionsIfNeeded(); } destroySession(DestroyReason::Quitting); } diff --git a/Telegram/SourceFiles/storage/serialize_common.cpp b/Telegram/SourceFiles/storage/serialize_common.cpp index d3f7ec8ad..3267ee1ee 100644 --- a/Telegram/SourceFiles/storage/serialize_common.cpp +++ b/Telegram/SourceFiles/storage/serialize_common.cpp @@ -9,6 +9,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Serialize { +ByteArrayWriter::ByteArrayWriter(int expectedSize) +: _stream(&_result, QIODevice::WriteOnly) { + if (expectedSize) { + _result.reserve(expectedSize); + } + _stream.setVersion(QDataStream::Qt_5_1); +} + +QByteArray ByteArrayWriter::result() && { + _stream.device()->close(); + return std::move(_result); +} + +ByteArrayReader::ByteArrayReader(QByteArray data) +: _data(std::move(data)) +, _stream(&_data, QIODevice::ReadOnly) { + _stream.setVersion(QDataStream::Qt_5_1); +} + void writeColor(QDataStream &stream, const QColor &color) { stream << (quint32(uchar(color.red())) | (quint32(uchar(color.green())) << 8) diff --git a/Telegram/SourceFiles/storage/serialize_common.h b/Telegram/SourceFiles/storage/serialize_common.h index fcf017185..c56081ab2 100644 --- a/Telegram/SourceFiles/storage/serialize_common.h +++ b/Telegram/SourceFiles/storage/serialize_common.h @@ -14,6 +14,70 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Serialize { +class ByteArrayWriter final { +public: + explicit ByteArrayWriter(int expectedSize = 0); + + [[nodiscard]] QDataStream &underlying() { + return _stream; + } + [[nodiscard]] operator QDataStream &() { + return _stream; + } + [[nodiscard]] QByteArray result() &&; + +private: + QByteArray _result; + QDataStream _stream; + +}; + +template +inline ByteArrayWriter &operator<<(ByteArrayWriter &stream, const T &data) { + stream.underlying() << data; + return stream; +} + +class ByteArrayReader final { +public: + explicit ByteArrayReader(QByteArray data); + + [[nodiscard]] QDataStream &underlying() { + return _stream; + } + [[nodiscard]] operator QDataStream &() { + return _stream; + } + + [[nodiscard]] bool atEnd() const { + return _stream.atEnd(); + } + [[nodiscard]] bool status() const { + return _stream.status(); + } + [[nodiscard]] bool ok() const { + return _stream.status() == QDataStream::Ok; + } + +private: + QByteArray _data; + QDataStream _stream; + +}; + +template +inline ByteArrayReader &operator>>(ByteArrayReader &stream, T &data) { + if (!stream.ok()) { + data = T(); + } else { + stream.underlying() >> data; + if (!stream.ok()) { + data = T(); + } + } + return stream; +} + inline int stringSize(const QString &str) { return sizeof(quint32) + str.size() * sizeof(ushort); } diff --git a/Telegram/SourceFiles/storage/serialize_peer.cpp b/Telegram/SourceFiles/storage/serialize_peer.cpp index 861bebdc9..96e9daec0 100644 --- a/Telegram/SourceFiles/storage/serialize_peer.cpp +++ b/Telegram/SourceFiles/storage/serialize_peer.cpp @@ -118,12 +118,16 @@ uint32 peerSize(not_null peer) { + imageLocationSize(peer->userpicLocation()) + sizeof(qint32); // userpic has video if (const auto user = peer->asUser()) { + const auto botInlinePlaceholder = user->isBot() + ? user->botInfo->inlinePlaceholder + : QString(); result += stringSize(user->firstName) + stringSize(user->lastName) + stringSize(user->phone()) + stringSize(user->username()) + sizeof(quint64) // access + sizeof(qint32) // flags + + stringSize(botInlinePlaceholder) + sizeof(quint32) // lastseen + sizeof(qint32) // contact + sizeof(qint32); // botInfoVersion diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index e3c2e69c4..6796179bd 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "core/core_settings.h" #include "core/file_location.h" +#include "data/components/top_peers.h" #include "data/stickers/data_stickers.h" #include "data/data_session.h" #include "data/data_document.h" @@ -42,6 +43,7 @@ using namespace details; using Database = Cache::Database; constexpr auto kDelayedWriteTimeout = crl::time(1000); +constexpr auto kWriteSearchSuggestionsDelay = 5 * crl::time(1000); constexpr auto kStickersVersionTag = quint32(-1); constexpr auto kStickersSerializeVersion = 4; @@ -87,6 +89,7 @@ enum { // Local Storage Keys lskSelfSerialized = 0x15, // serialized self lskMasksKeys = 0x16, // no data lskCustomEmojiKeys = 0x17, // no data + lskSearchSuggestions = 0x18, // no data }; auto EmptyMessageDraftSources() @@ -137,10 +140,13 @@ Account::Account(not_null owner, const QString &dataName) , _cacheTotalTimeLimit(Database::Settings().totalTimeLimit) , _cacheBigFileTotalTimeLimit(Database::Settings().totalTimeLimit) , _writeMapTimer([=] { writeMap(); }) -, _writeLocationsTimer([=] { writeLocations(); }) { +, _writeLocationsTimer([=] { writeLocations(); }) +, _writeSearchSuggestionsTimer([=] { writeSearchSuggestions(); }) { } Account::~Account() { + Expects(!_writeSearchSuggestionsTimer.isActive()); + if (_localKey && _mapChanged) { writeMap(); } @@ -209,6 +215,7 @@ base::flat_set Account::collectGoodNames() const { _installedCustomEmojiKey, _featuredCustomEmojiKey, _archivedCustomEmojiKey, + _searchSuggestionsKey, }; auto result = base::flat_set{ "map0", @@ -294,6 +301,7 @@ Account::ReadMapResult Account::readMapWith( quint64 savedGifsKey = 0; quint64 legacyBackgroundKeyDay = 0, legacyBackgroundKeyNight = 0; quint64 userSettingsKey = 0, recentHashtagsAndBotsKey = 0, exportSettingsKey = 0; + quint64 searchSuggestionsKey = 0; while (!map.stream.atEnd()) { quint32 keyType; map.stream >> keyType; @@ -399,6 +407,9 @@ Account::ReadMapResult Account::readMapWith( >> featuredCustomEmojiKey >> archivedCustomEmojiKey; } break; + case lskSearchSuggestions: { + map.stream >> searchSuggestionsKey; + } break; default: LOG(("App Error: unknown key type in encrypted map: %1").arg(keyType)); return ReadMapResult::Failed; @@ -434,6 +445,7 @@ Account::ReadMapResult Account::readMapWith( _settingsKey = userSettingsKey; _recentHashtagsAndBotsKey = recentHashtagsAndBotsKey; _exportSettingsKey = exportSettingsKey; + _searchSuggestionsKey = searchSuggestionsKey; _oldMapVersion = mapData.version; if (_oldMapVersion < AppVersion) { @@ -539,6 +551,7 @@ void Account::writeMap() { if (_installedCustomEmojiKey || _featuredCustomEmojiKey || _archivedCustomEmojiKey) { mapSize += sizeof(quint32) + 3 * sizeof(quint64); } + if (_searchSuggestionsKey) mapSize += sizeof(quint32) + sizeof(quint64); EncryptedDescriptor mapData(mapSize); if (!self.isEmpty()) { @@ -598,12 +611,18 @@ void Account::writeMap() { << quint64(_featuredCustomEmojiKey) << quint64(_archivedCustomEmojiKey); } + if (_searchSuggestionsKey) { + mapData.stream << quint32(lskSearchSuggestions); + mapData.stream << quint64(_searchSuggestionsKey); + } map.writeEncrypted(mapData, _localKey); _mapChanged = false; } void Account::reset() { + _writeSearchSuggestionsTimer.cancel(); + auto names = collectGoodNames(); _draftsMap.clear(); _draftCursorsMap.clear(); @@ -624,6 +643,7 @@ void Account::reset() { _archivedCustomEmojiKey = 0; _legacyBackgroundKeyDay = _legacyBackgroundKeyNight = 0; _settingsKey = _recentHashtagsAndBotsKey = _exportSettingsKey = 0; + _searchSuggestionsKey = 0; _oldMapVersion = 0; _fileLocations.clear(); _fileLocationPairs.clear(); @@ -2842,6 +2862,73 @@ Export::Settings Account::readExportSettings() { : Export::Settings(); } +void Account::writeSearchSuggestionsDelayed() { + Expects(_owner->sessionExists()); + + if (!_writeSearchSuggestionsTimer.isActive()) { + _writeSearchSuggestionsTimer.callOnce(kWriteSearchSuggestionsDelay); + } +} + +void Account::writeSearchSuggestionsIfNeeded() { + if (_writeSearchSuggestionsTimer.isActive()) { + _writeSearchSuggestionsTimer.cancel(); + writeSearchSuggestions(); + } +} + +void Account::writeSearchSuggestions() { + Expects(_owner->sessionExists()); + + const auto top = _owner->session().topPeers().serialize(); + const auto recent = QByteArray();// _owner->session().recentPeers().serialize(); + if (top.isEmpty() && recent.isEmpty()) { + if (_searchSuggestionsKey) { + ClearKey(_searchSuggestionsKey, _basePath); + _searchSuggestionsKey = 0; + writeMapDelayed(); + } + return; + } + if (!_searchSuggestionsKey) { + _searchSuggestionsKey = GenerateKey(_basePath); + writeMapQueued(); + } + quint32 size = Serialize::bytearraySize(top) + + Serialize::bytearraySize(recent); + EncryptedDescriptor data(size); + data.stream << top << recent; + + FileWriteDescriptor file(_searchSuggestionsKey, _basePath); + file.writeEncrypted(data, _localKey); +} + +void Account::readSearchSuggestions() { + if (_searchSuggestionsRead) { + return; + } + _searchSuggestionsRead = true; + if (!_searchSuggestionsKey) { + return; + } + + FileReadDescriptor suggestions; + if (!ReadEncryptedFile(suggestions, _searchSuggestionsKey, _basePath, _localKey)) { + ClearKey(_searchSuggestionsKey, _basePath); + _searchSuggestionsKey = 0; + writeMapDelayed(); + return; + } + + auto top = QByteArray(); + auto recent = QByteArray(); + suggestions.stream >> top >> recent; + if (CheckStreamStatus(suggestions.stream)) { + _owner->session().topPeers().applyLocal(top); + //_owner->session().recentPeers().applyLocal(recent); + } +} + void Account::writeSelf() { writeMapDelayed(); } diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h index ade41f10c..b22dd4cbb 100644 --- a/Telegram/SourceFiles/storage/storage_account.h +++ b/Telegram/SourceFiles/storage/storage_account.h @@ -148,6 +148,11 @@ public: void writeExportSettings(const Export::Settings &settings); [[nodiscard]] Export::Settings readExportSettings(); + void writeSearchSuggestionsDelayed(); + void writeSearchSuggestionsIfNeeded(); + void writeSearchSuggestions(); + void readSearchSuggestions(); + void writeSelf(); // Read self is special, it can't get session from account, because @@ -291,6 +296,7 @@ private: FileKey _installedCustomEmojiKey = 0; FileKey _featuredCustomEmojiKey = 0; FileKey _archivedCustomEmojiKey = 0; + FileKey _searchSuggestionsKey = 0; qint64 _cacheTotalSizeLimit = 0; qint64 _cacheBigFileTotalSizeLimit = 0; @@ -301,11 +307,13 @@ private: bool _trustedBotsRead = false; bool _readingUserSettings = false; bool _recentHashtagsAndBotsWereRead = false; + bool _searchSuggestionsRead = false; int _oldMapVersion = 0; base::Timer _writeMapTimer; base::Timer _writeLocationsTimer; + base::Timer _writeSearchSuggestionsTimer; bool _mapChanged = false; bool _locationsChanged = false;