1 /******************************************************************************
2 * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19 #include "syncdata.h"
20
21 #include "events/eventloader.h"
22
23 #include <QtCore/QFile>
24 #include <QtCore/QFileInfo>
25
26 using namespace Quotient;
27
28 const QString SyncRoomData::UnreadCountKey =
29 QStringLiteral("x-quotient.unread_count");
30
isEmpty() const31 bool RoomSummary::isEmpty() const
32 {
33 return !joinedMemberCount && !invitedMemberCount && !heroes;
34 }
35
merge(const RoomSummary & other)36 bool RoomSummary::merge(const RoomSummary& other)
37 {
38 // Using bitwise OR to prevent computation shortcut.
39 return joinedMemberCount.merge(other.joinedMemberCount)
40 | invitedMemberCount.merge(other.invitedMemberCount)
41 | heroes.merge(other.heroes);
42 }
43
operator <<(QDebug dbg,const RoomSummary & rs)44 QDebug Quotient::operator<<(QDebug dbg, const RoomSummary& rs)
45 {
46 QDebugStateSaver _(dbg);
47 QStringList sl;
48 if (rs.joinedMemberCount)
49 sl << QStringLiteral("joined: %1").arg(*rs.joinedMemberCount);
50 if (rs.invitedMemberCount)
51 sl << QStringLiteral("invited: %1").arg(*rs.invitedMemberCount);
52 if (rs.heroes)
53 sl << QStringLiteral("heroes: [%1]").arg(rs.heroes->join(','));
54 dbg.nospace().noquote() << sl.join(QStringLiteral("; "));
55 return dbg;
56 }
57
dumpTo(QJsonObject & jo,const RoomSummary & rs)58 void JsonObjectConverter<RoomSummary>::dumpTo(QJsonObject& jo,
59 const RoomSummary& rs)
60 {
61 addParam<IfNotEmpty>(jo, QStringLiteral("m.joined_member_count"),
62 rs.joinedMemberCount);
63 addParam<IfNotEmpty>(jo, QStringLiteral("m.invited_member_count"),
64 rs.invitedMemberCount);
65 addParam<IfNotEmpty>(jo, QStringLiteral("m.heroes"), rs.heroes);
66 }
67
fillFrom(const QJsonObject & jo,RoomSummary & rs)68 void JsonObjectConverter<RoomSummary>::fillFrom(const QJsonObject& jo,
69 RoomSummary& rs)
70 {
71 fromJson(jo["m.joined_member_count"_ls], rs.joinedMemberCount);
72 fromJson(jo["m.invited_member_count"_ls], rs.invitedMemberCount);
73 fromJson(jo["m.heroes"_ls], rs.heroes);
74 }
75
76 template <typename EventsArrayT, typename StrT>
load(const QJsonObject & batches,StrT keyName)77 inline EventsArrayT load(const QJsonObject& batches, StrT keyName)
78 {
79 return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls));
80 }
81
SyncRoomData(const QString & roomId_,JoinState joinState_,const QJsonObject & room_)82 SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
83 const QJsonObject& room_)
84 : roomId(roomId_)
85 , joinState(joinState_)
86 , summary(fromJson<RoomSummary>(room_["summary"_ls]))
87 , state(load<StateEvents>(room_, joinState == JoinState::Invite
88 ? "invite_state"_ls
89 : "state"_ls))
90 {
91 switch (joinState) {
92 case JoinState::Join:
93 ephemeral = load<Events>(room_, "ephemeral"_ls);
94 [[fallthrough]];
95 case JoinState::Leave: {
96 accountData = load<Events>(room_, "account_data"_ls);
97 timeline = load<RoomEvents>(room_, "timeline"_ls);
98 const auto timelineJson = room_.value("timeline"_ls).toObject();
99 timelineLimited = timelineJson.value("limited"_ls).toBool();
100 timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString();
101
102 break;
103 }
104 default: /* nothing on top of state */;
105 }
106
107 const auto unreadJson = room_.value("unread_notifications"_ls).toObject();
108 unreadCount = unreadJson.value(UnreadCountKey).toInt(-2);
109 highlightCount = unreadJson.value("highlight_count"_ls).toInt();
110 notificationCount = unreadJson.value("notification_count"_ls).toInt();
111 if (highlightCount > 0 || notificationCount > 0)
112 qCDebug(SYNCJOB) << "Room" << roomId_
113 << "has highlights:" << highlightCount
114 << "and notifications:" << notificationCount;
115 }
116
SyncData(const QString & cacheFileName)117 SyncData::SyncData(const QString& cacheFileName)
118 {
119 QFileInfo cacheFileInfo { cacheFileName };
120 auto json = loadJson(cacheFileName);
121 auto requiredVersion = std::get<0>(cacheVersion());
122 auto actualVersion =
123 json.value("cache_version"_ls).toObject().value("major"_ls).toInt();
124 if (actualVersion == requiredVersion)
125 parseJson(json, cacheFileInfo.absolutePath() + '/');
126 else
127 qCWarning(MAIN) << "Major version of the cache file is" << actualVersion
128 << "but" << requiredVersion
129 << "is required; discarding the cache";
130 }
131
takeRoomData()132 SyncDataList&& SyncData::takeRoomData() { return move(roomData); }
133
fileNameForRoom(QString roomId)134 QString SyncData::fileNameForRoom(QString roomId)
135 {
136 roomId.replace(':', '_');
137 return roomId + ".json";
138 }
139
takePresenceData()140 Events&& SyncData::takePresenceData() { return std::move(presenceData); }
141
takeAccountData()142 Events&& SyncData::takeAccountData() { return std::move(accountData); }
143
takeToDeviceEvents()144 Events&& SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); }
145
loadJson(const QString & fileName)146 QJsonObject SyncData::loadJson(const QString& fileName)
147 {
148 QFile roomFile { fileName };
149 if (!roomFile.exists()) {
150 qCWarning(MAIN) << "No state cache file" << fileName;
151 return {};
152 }
153 if (!roomFile.open(QIODevice::ReadOnly)) {
154 qCWarning(MAIN) << "Failed to open state cache file"
155 << roomFile.fileName();
156 return {};
157 }
158 auto data = roomFile.readAll();
159
160 const auto json = (data.startsWith('{')
161 ? QJsonDocument::fromJson(data)
162 : QJsonDocument::fromBinaryData(data))
163 .object();
164 if (json.isEmpty()) {
165 qCWarning(MAIN) << "State cache in" << fileName
166 << "is broken or empty, discarding";
167 }
168 return json;
169 }
170
parseJson(const QJsonObject & json,const QString & baseDir)171 void SyncData::parseJson(const QJsonObject& json, const QString& baseDir)
172 {
173 QElapsedTimer et;
174 et.start();
175
176 nextBatch_ = json.value("next_batch"_ls).toString();
177 presenceData = load<Events>(json, "presence"_ls);
178 accountData = load<Events>(json, "account_data"_ls);
179 toDeviceEvents = load<Events>(json, "to_device"_ls);
180
181 auto deviceOneTimeKeysCountVariantHash =
182 json.value("device_one_time_keys_count"_ls).toObject().toVariantHash();
183 for (auto key : deviceOneTimeKeysCountVariantHash.keys()) {
184 deviceOneTimeKeysCount_.insert(
185 key, deviceOneTimeKeysCountVariantHash.value(key).toInt());
186 }
187
188 auto rooms = json.value("rooms"_ls).toObject();
189 JoinStates::Int ii = 1; // ii is used to make a JoinState value
190 auto totalRooms = 0;
191 auto totalEvents = 0;
192 for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) {
193 const auto rs = rooms.value(JoinStateStrings[i]).toObject();
194 // We have a Qt container on the right and an STL one on the left
195 roomData.reserve(static_cast<size_t>(rs.size()));
196 for (auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) {
197 auto roomJson =
198 roomIt->isObject()
199 ? roomIt->toObject()
200 : loadJson(baseDir + fileNameForRoom(roomIt.key()));
201 if (roomJson.isEmpty()) {
202 unresolvedRoomIds.push_back(roomIt.key());
203 continue;
204 }
205 roomData.emplace_back(roomIt.key(), JoinState(ii), roomJson);
206 const auto& r = roomData.back();
207 totalEvents += r.state.size() + r.ephemeral.size()
208 + r.accountData.size() + r.timeline.size();
209 }
210 totalRooms += rs.size();
211 }
212 if (!unresolvedRoomIds.empty())
213 qCWarning(MAIN) << "Unresolved rooms:" << unresolvedRoomIds.join(',');
214 if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs())
215 qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with"
216 << totalRooms << "room(s)," << totalEvents
217 << "event(s) in" << et;
218 }
219