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