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