| 1 | // SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> |
| 2 | // SPDX-License-Identifier: LGPL-2.1-or-later |
| 3 | |
| 4 | #include "syncdata.h" |
| 5 | |
| 6 | #include "logging_categories_p.h" |
| 7 | |
| 8 | #include <QtCore/QFile> |
| 9 | #include <QtCore/QFileInfo> |
| 10 | |
| 11 | using namespace Quotient; |
| 12 | |
| 13 | bool RoomSummary::isEmpty() const |
| 14 | { |
| 15 | return !joinedMemberCount && !invitedMemberCount && !heroes; |
| 16 | } |
| 17 | |
| 18 | bool RoomSummary::merge(const RoomSummary& other) |
| 19 | { |
| 20 | // Using bitwise OR to prevent computation shortcut. |
| 21 | return static_cast<bool>( |
| 22 | static_cast<int>(joinedMemberCount.merge(other: other.joinedMemberCount)) |
| 23 | | static_cast<int>(invitedMemberCount.merge(other: other.invitedMemberCount)) |
| 24 | | static_cast<int>(heroes.merge(other: other.heroes))); |
| 25 | } |
| 26 | |
| 27 | QDebug Quotient::operator<<(QDebug dbg, const RoomSummary& rs) |
| 28 | { |
| 29 | QDebugStateSaver _(dbg); |
| 30 | QStringList sl; |
| 31 | if (rs.joinedMemberCount) |
| 32 | sl << QStringLiteral("joined: %1" ).arg(a: *rs.joinedMemberCount); |
| 33 | if (rs.invitedMemberCount) |
| 34 | sl << QStringLiteral("invited: %1" ).arg(a: *rs.invitedMemberCount); |
| 35 | if (rs.heroes) |
| 36 | sl << QStringLiteral("heroes: [%1]" ).arg(a: rs.heroes->join(sep: u',')); |
| 37 | dbg.nospace().noquote() << sl.join(QStringLiteral("; " )); |
| 38 | return dbg; |
| 39 | } |
| 40 | |
| 41 | void JsonObjectConverter<RoomSummary>::dumpTo(QJsonObject& jo, |
| 42 | const RoomSummary& rs) |
| 43 | { |
| 44 | addParam<IfNotEmpty>(container&: jo, QStringLiteral("m.joined_member_count" ), |
| 45 | value: rs.joinedMemberCount); |
| 46 | addParam<IfNotEmpty>(container&: jo, QStringLiteral("m.invited_member_count" ), |
| 47 | value: rs.invitedMemberCount); |
| 48 | addParam<IfNotEmpty>(container&: jo, QStringLiteral("m.heroes" ), value: rs.heroes); |
| 49 | } |
| 50 | |
| 51 | void JsonObjectConverter<RoomSummary>::fillFrom(const QJsonObject& jo, |
| 52 | RoomSummary& rs) |
| 53 | { |
| 54 | fromJson(json: jo["m.joined_member_count"_ls ], pod&: rs.joinedMemberCount); |
| 55 | fromJson(json: jo["m.invited_member_count"_ls ], pod&: rs.invitedMemberCount); |
| 56 | fromJson(json: jo["m.heroes"_ls ], pod&: rs.heroes); |
| 57 | } |
| 58 | |
| 59 | template <typename EventsArrayT, typename StrT> |
| 60 | inline EventsArrayT load(const QJsonObject& batches, StrT keyName) |
| 61 | { |
| 62 | return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls )); |
| 63 | } |
| 64 | |
| 65 | SyncRoomData::SyncRoomData(QString roomId_, JoinState joinState, |
| 66 | const QJsonObject& roomJson) |
| 67 | : roomId(std::move(roomId_)) |
| 68 | , joinState(joinState) |
| 69 | , summary(fromJson<RoomSummary>(json: roomJson["summary"_ls ])) |
| 70 | , state(load<StateEvents>(batches: roomJson, keyName: joinState == JoinState::Invite |
| 71 | ? "invite_state"_ls |
| 72 | : "state"_ls )) |
| 73 | { |
| 74 | switch (joinState) { |
| 75 | case JoinState::Join: |
| 76 | ephemeral = load<Events>(batches: roomJson, keyName: "ephemeral"_ls ); |
| 77 | [[fallthrough]]; |
| 78 | case JoinState::Leave: { |
| 79 | accountData = load<Events>(batches: roomJson, keyName: "account_data"_ls ); |
| 80 | timeline = load<RoomEvents>(batches: roomJson, keyName: "timeline"_ls ); |
| 81 | const auto timelineJson = roomJson.value(key: "timeline"_ls ).toObject(); |
| 82 | timelineLimited = timelineJson.value(key: "limited"_ls ).toBool(); |
| 83 | timelinePrevBatch = timelineJson.value(key: "prev_batch"_ls ).toString(); |
| 84 | |
| 85 | break; |
| 86 | } |
| 87 | default: /* nothing on top of state */; |
| 88 | } |
| 89 | |
| 90 | const auto unreadJson = roomJson.value(key: UnreadNotificationsKey).toObject(); |
| 91 | |
| 92 | fromJson(json: unreadJson.value(key: PartiallyReadCountKey), pod&: partiallyReadCount); |
| 93 | if (!partiallyReadCount.has_value()) |
| 94 | fromJson(json: unreadJson.value(key: "x-quotient.unread_count"_ls ), |
| 95 | pod&: partiallyReadCount); |
| 96 | |
| 97 | fromJson(json: roomJson.value(key: NewUnreadCountKey), pod&: unreadCount); |
| 98 | if (!unreadCount.has_value()) |
| 99 | fromJson(json: unreadJson.value(key: "notification_count"_ls ), pod&: unreadCount); |
| 100 | fromJson(json: unreadJson.value(key: HighlightCountKey), pod&: highlightCount); |
| 101 | } |
| 102 | |
| 103 | QDebug Quotient::operator<<(QDebug dbg, const DevicesList& devicesList) |
| 104 | { |
| 105 | QDebugStateSaver _(dbg); |
| 106 | QStringList sl; |
| 107 | if (!devicesList.changed.isEmpty()) |
| 108 | sl << QStringLiteral("changed: %1" ).arg(a: devicesList.changed.join(sep: ", "_ls )); |
| 109 | if (!devicesList.left.isEmpty()) |
| 110 | sl << QStringLiteral("left %1" ).arg(a: devicesList.left.join(sep: ", "_ls )); |
| 111 | dbg.nospace().noquote() << sl.join(QStringLiteral("; " )); |
| 112 | return dbg; |
| 113 | } |
| 114 | |
| 115 | void JsonObjectConverter<DevicesList>::dumpTo(QJsonObject& jo, |
| 116 | const DevicesList& rs) |
| 117 | { |
| 118 | addParam<IfNotEmpty>(container&: jo, QStringLiteral("changed" ), |
| 119 | value: rs.changed); |
| 120 | addParam<IfNotEmpty>(container&: jo, QStringLiteral("left" ), |
| 121 | value: rs.left); |
| 122 | } |
| 123 | |
| 124 | void JsonObjectConverter<DevicesList>::fillFrom(const QJsonObject& jo, |
| 125 | DevicesList& rs) |
| 126 | { |
| 127 | fromJson(json: jo["changed"_ls ], pod&: rs.changed); |
| 128 | fromJson(json: jo["left"_ls ], pod&: rs.left); |
| 129 | } |
| 130 | |
| 131 | SyncData::SyncData(const QString& cacheFileName) |
| 132 | { |
| 133 | QFileInfo cacheFileInfo { cacheFileName }; |
| 134 | auto json = loadJson(fileName: cacheFileName); |
| 135 | auto requiredVersion = MajorCacheVersion; |
| 136 | auto actualVersion = |
| 137 | json.value(key: "cache_version"_ls ).toObject().value(key: "major"_ls ).toInt(); |
| 138 | if (actualVersion == requiredVersion) |
| 139 | parseJson(json, baseDir: cacheFileInfo.absolutePath() + u'/'); |
| 140 | else |
| 141 | qCWarning(MAIN) << "Major version of the cache file is" << actualVersion |
| 142 | << "but" << requiredVersion |
| 143 | << "is required; discarding the cache" ; |
| 144 | } |
| 145 | |
| 146 | SyncDataList SyncData::takeRoomData() { return std::move(roomData); } |
| 147 | |
| 148 | QString SyncData::fileNameForRoom(QString roomId) |
| 149 | { |
| 150 | roomId.replace(before: u':', after: u'_'); |
| 151 | return roomId + ".json"_ls ; |
| 152 | } |
| 153 | |
| 154 | Events SyncData::takePresenceData() { return std::move(presenceData); } |
| 155 | |
| 156 | Events SyncData::takeAccountData() { return std::move(accountData); } |
| 157 | |
| 158 | Events SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); } |
| 159 | |
| 160 | std::pair<int, int> SyncData::cacheVersion() |
| 161 | { |
| 162 | return { MajorCacheVersion, 2 }; |
| 163 | } |
| 164 | |
| 165 | DevicesList SyncData::takeDevicesList() { return std::move(devicesList); } |
| 166 | |
| 167 | QJsonObject SyncData::loadJson(const QString& fileName) |
| 168 | { |
| 169 | QFile roomFile { fileName }; |
| 170 | if (!roomFile.exists()) { |
| 171 | qCWarning(MAIN) << "No state cache file" << fileName; |
| 172 | return {}; |
| 173 | } |
| 174 | if (!roomFile.open(flags: QIODevice::ReadOnly)) { |
| 175 | qCWarning(MAIN) << "Failed to open state cache file" |
| 176 | << roomFile.fileName(); |
| 177 | return {}; |
| 178 | } |
| 179 | auto data = roomFile.readAll(); |
| 180 | |
| 181 | const auto json = data.startsWith(c: '{') |
| 182 | ? QJsonDocument::fromJson(json: data).object() |
| 183 | : QCborValue::fromCbor(ba: data).toJsonValue().toObject(); |
| 184 | if (json.isEmpty()) { |
| 185 | qCWarning(MAIN) << "State cache in" << fileName |
| 186 | << "is broken or empty, discarding" ; |
| 187 | } |
| 188 | return json; |
| 189 | } |
| 190 | |
| 191 | void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) |
| 192 | { |
| 193 | QElapsedTimer et; |
| 194 | et.start(); |
| 195 | |
| 196 | nextBatch_ = json.value(key: "next_batch"_ls ).toString(); |
| 197 | presenceData = load<Events>(batches: json, keyName: "presence"_ls ); |
| 198 | accountData = load<Events>(batches: json, keyName: "account_data"_ls ); |
| 199 | toDeviceEvents = load<Events>(batches: json, keyName: "to_device"_ls ); |
| 200 | |
| 201 | fromJson(json: json.value(key: "device_one_time_keys_count"_ls ), |
| 202 | pod&: deviceOneTimeKeysCount_); |
| 203 | |
| 204 | if(json.contains(key: "device_lists"_ls )) { |
| 205 | fromJson(json: json.value(key: "device_lists"_ls ), pod&: devicesList); |
| 206 | } |
| 207 | |
| 208 | auto rooms = json.value(key: "rooms"_ls ).toObject(); |
| 209 | auto totalRooms = 0; |
| 210 | auto totalEvents = 0; |
| 211 | for (size_t i = 0; i < JoinStateStrings.size(); ++i) { |
| 212 | // This assumes that MemberState values go over powers of 2: 1,2,4,... |
| 213 | const auto joinState = JoinState(1U << i); |
| 214 | const auto rs = rooms.value(key: JoinStateStrings[i]).toObject(); |
| 215 | // We have a Qt container on the right and an STL one on the left |
| 216 | roomData.reserve(n: roomData.size() + static_cast<size_t>(rs.size())); |
| 217 | for (auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) { |
| 218 | QJsonObject roomJson; |
| 219 | if (!baseDir.isEmpty()) { |
| 220 | // Loading data from the local cache, with room objects saved in |
| 221 | // individual files rather than inline |
| 222 | roomJson = loadJson(fileName: baseDir + fileNameForRoom(roomId: roomIt.key())); |
| 223 | if (roomJson.isEmpty()) { |
| 224 | unresolvedRoomIds.push_back(t: roomIt.key()); |
| 225 | continue; |
| 226 | } |
| 227 | } else // When loading from /sync response, everything is inline |
| 228 | roomJson = roomIt->toObject(); |
| 229 | |
| 230 | roomData.emplace_back(args: roomIt.key(), args: joinState, args&: roomJson); |
| 231 | const auto& r = roomData.back(); |
| 232 | totalEvents += r.state.size() + r.ephemeral.size() |
| 233 | + r.accountData.size() + r.timeline.size(); |
| 234 | } |
| 235 | totalRooms += rs.size(); |
| 236 | } |
| 237 | if (!unresolvedRoomIds.empty()) |
| 238 | qCWarning(MAIN) << "Unresolved rooms:" << unresolvedRoomIds.join(sep: u','); |
| 239 | if (totalRooms > 9 || et.nsecsElapsed() >= ProfilerMinNsecs) |
| 240 | qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" |
| 241 | << totalRooms << "room(s)," << totalEvents |
| 242 | << "event(s) in" << et; |
| 243 | } |
| 244 | |