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
11using namespace Quotient;
12
13bool RoomSummary::isEmpty() const
14{
15 return !joinedMemberCount && !invitedMemberCount && !heroes;
16}
17
18bool 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
27QDebug 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
41void 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
51void 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
59template <typename EventsArrayT, typename StrT>
60inline EventsArrayT load(const QJsonObject& batches, StrT keyName)
61{
62 return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls));
63}
64
65SyncRoomData::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
103QDebug 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
115void 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
124void 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
131SyncData::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
146SyncDataList SyncData::takeRoomData() { return std::move(roomData); }
147
148QString SyncData::fileNameForRoom(QString roomId)
149{
150 roomId.replace(before: u':', after: u'_');
151 return roomId + ".json"_ls;
152}
153
154Events SyncData::takePresenceData() { return std::move(presenceData); }
155
156Events SyncData::takeAccountData() { return std::move(accountData); }
157
158Events SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); }
159
160std::pair<int, int> SyncData::cacheVersion()
161{
162 return { MajorCacheVersion, 2 };
163}
164
165DevicesList SyncData::takeDevicesList() { return std::move(devicesList); }
166
167QJsonObject 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
191void 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