1 /******************************************************************************
2  * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
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 "room.h"
20 
21 #include "avatar.h"
22 #include "connection.h"
23 #include "converters.h"
24 #include "e2ee.h"
25 #include "syncdata.h"
26 #include "user.h"
27 
28 #include "csapi/account-data.h"
29 #include "csapi/banning.h"
30 #include "csapi/inviting.h"
31 #include "csapi/kicking.h"
32 #include "csapi/leaving.h"
33 #include "csapi/receipts.h"
34 #include "csapi/read_markers.h"
35 #include "csapi/redaction.h"
36 #include "csapi/room_send.h"
37 #include "csapi/room_state.h"
38 #include "csapi/room_upgrades.h"
39 #include "csapi/rooms.h"
40 #include "csapi/tags.h"
41 
42 #include "events/callanswerevent.h"
43 #include "events/callcandidatesevent.h"
44 #include "events/callhangupevent.h"
45 #include "events/callinviteevent.h"
46 #include "events/encryptionevent.h"
47 #include "events/reactionevent.h"
48 #include "events/receiptevent.h"
49 #include "events/redactionevent.h"
50 #include "events/roomavatarevent.h"
51 #include "events/roomcreateevent.h"
52 #include "events/roommemberevent.h"
53 #include "events/roomtombstoneevent.h"
54 #include "events/simplestateevents.h"
55 #include "events/typingevent.h"
56 #include "events/roompowerlevelsevent.h"
57 #include "jobs/downloadfilejob.h"
58 #include "jobs/mediathumbnailjob.h"
59 #include "events/roomcanonicalaliasevent.h"
60 
61 #include <QtCore/QDir>
62 #include <QtCore/QHash>
63 #include <QtCore/QMimeDatabase>
64 #include <QtCore/QPointer>
65 #include <QtCore/QRegularExpression>
66 #include <QtCore/QStringBuilder> // for efficient string concats (operator%)
67 #include <QtCore/QTemporaryFile>
68 
69 #include <array>
70 #include <cmath>
71 #include <functional>
72 
73 #ifdef Quotient_E2EE_ENABLED
74 #include <account.h> // QtOlm
75 #include <errors.h> // QtOlm
76 #include <groupsession.h> // QtOlm
77 #endif // Quotient_E2EE_ENABLED
78 
79 using namespace Quotient;
80 using namespace QtOlm;
81 using namespace std::placeholders;
82 using std::move;
83 #if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123)
84 using std::llround;
85 #endif
86 
87 enum EventsPlacement : int { Older = -1, Newer = 1 };
88 
89 class Room::Private {
90 public:
91     /// Map of user names to users
92     /** User names potentially duplicate, hence QMultiHash. */
93     using members_map_t = QMultiHash<QString, User*>;
94 
Private(Connection * c,QString id_,JoinState initialJoinState)95     Private(Connection* c, QString id_, JoinState initialJoinState)
96         : q(nullptr), connection(c), id(move(id_)), joinState(initialJoinState)
97     {}
98 
99     Room* q;
100 
101     Connection* connection;
102     QString id;
103     JoinState joinState;
104     RoomSummary summary = { none, 0, none };
105     /// The state of the room at timeline position before-0
106     /// \sa timelineBase
107     UnorderedMap<StateEventKey, StateEventPtr> baseState;
108     /// State event stubs - events without content, just type and state key
109     static decltype(baseState) stubbedState;
110     /// The state of the room at syncEdge()
111     /// \sa syncEdge
112     QHash<StateEventKey, const StateEventBase*> currentState;
113     /// Servers with aliases for this room except the one of the local user
114     /// \sa Room::remoteAliases
115     QSet<QString> aliasServers;
116 
117     Timeline timeline;
118     PendingEvents unsyncedEvents;
119     QHash<QString, TimelineItem::index_t> eventsIndex;
120     // A map from evtId to a map of relation type to a vector of event
121     // pointers. Not using QMultiHash, because we want to quickly return
122     // a number of relations for a given event without enumerating them.
123     QHash<QPair<QString, QString>, RelatedEvents> relations;
124     QString displayname;
125     Avatar avatar;
126     int highlightCount = 0;
127     int notificationCount = 0;
128     members_map_t membersMap;
129     QList<User*> usersTyping;
130     QMultiHash<QString, User*> eventIdReadUsers;
131     QList<User*> usersInvited;
132     QList<User*> membersLeft;
133     int unreadMessages = 0;
134     bool displayed = false;
135     QString firstDisplayedEventId;
136     QString lastDisplayedEventId;
137     QHash<const User*, QString> lastReadEventIds;
138     QString fullyReadUntilEventId;
139     TagsMap tags;
140     UnorderedMap<QString, EventPtr> accountData;
141     QString prevBatch;
142     QPointer<GetRoomEventsJob> eventsHistoryJob;
143     QPointer<GetMembersByRoomJob> allMembersJob;
144 
145     struct FileTransferPrivateInfo {
146         FileTransferPrivateInfo() = default;
FileTransferPrivateInfoRoom::Private::FileTransferPrivateInfo147         FileTransferPrivateInfo(BaseJob* j, const QString& fileName,
148                                 bool isUploading = false)
149             : status(FileTransferInfo::Started)
150             , job(j)
151             , localFileInfo(fileName)
152             , isUpload(isUploading)
153         {}
154 
155         FileTransferInfo::Status status = FileTransferInfo::None;
156         QPointer<BaseJob> job = nullptr;
157         QFileInfo localFileInfo {};
158         bool isUpload = false;
159         qint64 progress = 0;
160         qint64 total = -1;
161 
updateRoom::Private::FileTransferPrivateInfo162         void update(qint64 p, qint64 t)
163         {
164             if (t == 0) {
165                 t = -1;
166                 if (p == 0)
167                     p = -1;
168             }
169             if (p != -1)
170                 qCDebug(PROFILER) << "Transfer progress:" << p << "/" << t
171                                   << "=" << llround(double(p) / t * 100) << "%";
172             progress = p;
173             total = t;
174         }
175     };
failedTransfer(const QString & tid,const QString & errorMessage={})176     void failedTransfer(const QString& tid, const QString& errorMessage = {})
177     {
178         qCWarning(MAIN) << "File transfer failed for id" << tid;
179         if (!errorMessage.isEmpty())
180             qCWarning(MAIN) << "Message:" << errorMessage;
181         fileTransfers[tid].status = FileTransferInfo::Failed;
182         emit q->fileTransferFailed(tid, errorMessage);
183     }
184     /// A map from event/txn ids to information about the long operation;
185     /// used for both download and upload operations
186     QHash<QString, FileTransferPrivateInfo> fileTransfers;
187 
188     const RoomMessageEvent* getEventWithFile(const QString& eventId) const;
189     QString fileNameToDownload(const RoomMessageEvent* event) const;
190 
191     Changes setSummary(RoomSummary&& newSummary);
192 
193     // void inviteUser(User* u); // We might get it at some point in time.
194     void insertMemberIntoMap(User* u);
195     void removeMemberFromMap(User* u);
196 
197     // This updates the room displayname field (which is the way a room
198     // should be shown in the room list); called whenever the list of
199     // members, the room name (m.room.name) or canonical alias change.
200     void updateDisplayname();
201     // This is used by updateDisplayname() but only calculates the new name
202     // without any updates.
203     QString calculateDisplayname() const;
204 
205     /// A point in the timeline corresponding to baseState
timelineBase() const206     rev_iter_t timelineBase() const { return q->findInTimeline(-1); }
historyEdge() const207     rev_iter_t historyEdge() const { return timeline.crend(); }
syncEdge() const208     Timeline::const_iterator syncEdge() const { return timeline.cend(); }
209 
210     void getPreviousContent(int limit = 10);
211 
getCurrentState(const StateEventKey & evtKey) const212     const StateEventBase* getCurrentState(const StateEventKey& evtKey) const
213     {
214         const auto* evt = currentState.value(evtKey, nullptr);
215         if (!evt) {
216             if (stubbedState.find(evtKey) == stubbedState.end()) {
217                 // In the absence of a real event, make a stub as-if an event
218                 // with empty content has been received. Event classes should be
219                 // prepared for empty/invalid/malicious content anyway.
220                 stubbedState.emplace(evtKey, loadStateEvent(evtKey.first, {},
221                                                             evtKey.second));
222                 qCDebug(STATE) << "A new stub event created for key {"
223                                << evtKey.first << evtKey.second << "}";
224             }
225             evt = stubbedState[evtKey].get();
226             Q_ASSERT(evt);
227         }
228         Q_ASSERT(evt->matrixType() == evtKey.first
229                  && evt->stateKey() == evtKey.second);
230         return evt;
231     }
232 
233     template <typename EventT>
getCurrentState(const QString & stateKey={}) const234     const EventT* getCurrentState(const QString& stateKey = {}) const
235     {
236         const StateEventKey evtKey { EventT::matrixTypeId(), stateKey };
237         const auto* evt = currentState.value(evtKey, nullptr);
238         if (!evt) {
239             if (stubbedState.find(evtKey) == stubbedState.end()) {
240                 // In the absence of a real event, make a stub as-if an event
241                 // with empty content has been received. Event classes should be
242                 // prepared for empty/invalid/malicious content anyway.
243                 stubbedState.emplace(
244                     evtKey, makeEvent<EventT>(basicStateEventJson(
245                                 EventT::matrixTypeId(), {}, evtKey.second)));
246                 qCDebug(STATE) << "A new stub event created for key {"
247                                << evtKey.first << evtKey.second << "}";
248             }
249             evt = stubbedState[evtKey].get();
250             Q_ASSERT(evt);
251         }
252         Q_ASSERT(evt->type() == EventT::typeId()
253                  && evt->matrixType() == EventT::matrixTypeId()
254                  && evt->stateKey() == stateKey);
255         return static_cast<const EventT*>(evt);
256     }
257 
258 //    template <typename EventT>
259 //    const auto& getCurrentStateContent(const QString& stateKey = {}) const
260 //    {
261 //        if (const auto* evt =
262 //                currentState.value({ EventT::matrixTypeId(), stateKey }, nullptr))
263 //            return evt->content();
264 //        return EventT::content_type()
265 //    }
266 
isEventNotable(const TimelineItem & ti) const267     bool isEventNotable(const TimelineItem& ti) const
268     {
269         return !ti->isRedacted() && ti->senderId() != connection->userId()
270                && is<RoomMessageEvent>(*ti)
271                && ti.viewAs<RoomMessageEvent>()->replacedEvent().isEmpty();
272     }
273 
274     template <typename EventArrayT>
updateStateFrom(EventArrayT && events)275     Changes updateStateFrom(EventArrayT&& events)
276     {
277         Changes changes = NoChange;
278         if (!events.empty()) {
279             QElapsedTimer et;
280             et.start();
281             for (auto&& eptr : events) {
282                 const auto& evt = *eptr;
283                 Q_ASSERT(evt.isStateEvent());
284                 // Update baseState afterwards to make sure that the old state
285                 // is valid and usable inside processStateEvent
286                 changes |= q->processStateEvent(evt);
287                 baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr);
288             }
289             if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
290                 qCDebug(PROFILER)
291                     << "Updated" << q->objectName() << "room state from"
292                     << events.size() << "event(s) in" << et;
293         }
294         return changes;
295     }
296     Changes addNewMessageEvents(RoomEvents&& events);
297     void addHistoricalMessageEvents(RoomEvents&& events);
298 
299     /** Move events into the timeline
300      *
301      * Insert events into the timeline, either new or historical.
302      * Pointers in the original container become empty, the ownership
303      * is passed to the timeline container.
304      * @param events - the range of events to be inserted
305      * @param placement - position and direction of insertion: Older for
306      *                    historical messages, Newer for new ones
307      */
308     Timeline::size_type moveEventsToTimeline(RoomEventsRange events,
309                                              EventsPlacement placement);
310 
311     /**
312      * Remove events from the passed container that are already in the timeline
313      */
314     void dropDuplicateEvents(RoomEvents& events) const;
315 
316     void setLastReadReceipt(User* u, rev_iter_t newMarker,
317                             QString newEvtId = {});
318     Changes setFullyReadMarker(const QString &eventId);
319     Changes updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to);
320     Changes recalculateUnreadCount(bool force = false);
321     void markMessagesAsRead(const rev_iter_t &upToMarker);
322 
323     void getAllMembers();
324 
325     QString sendEvent(RoomEventPtr&& event);
326 
327     template <typename EventT, typename... ArgTs>
sendEvent(ArgTs &&...eventArgs)328     QString sendEvent(ArgTs&&... eventArgs)
329     {
330         return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...));
331     }
332 
333     RoomEvent* addAsPending(RoomEventPtr&& event);
334 
335     QString doSendEvent(const RoomEvent* pEvent);
336     void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr);
337 
requestSetState(const StateEventBase & event)338     SetRoomStateWithKeyJob* requestSetState(const StateEventBase& event)
339     {
340         //            if (event.roomId().isEmpty())
341         //                event.setRoomId(id);
342         //            if (event.senderId().isEmpty())
343         //                event.setSender(connection->userId());
344         // TODO: Queue up state events sending (see #133).
345         // TODO: Maybe addAsPending() as well, despite having no txnId
346         return connection->callApi<SetRoomStateWithKeyJob>(
347             id, event.matrixType(), event.stateKey(), event.contentJson());
348     }
349 
350     template <typename EvT, typename... ArgTs>
requestSetState(ArgTs &&...args)351     auto requestSetState(ArgTs&&... args)
352     {
353         return requestSetState(EvT(std::forward<ArgTs>(args)...));
354     }
355 
356     /*! Apply redaction to the timeline
357      *
358      * Tries to find an event in the timeline and redact it; deletes the
359      * redaction event whether the redacted event was found or not.
360      * \return true if the event has been found and redacted; false otherwise
361      */
362     bool processRedaction(const RedactionEvent& redaction);
363 
364     /*! Apply a new revision of the event to the timeline
365      *
366      * Tries to find an event in the timeline and replace it with the new
367      * content passed in \p newMessage.
368      * \return true if the event has been found and replaced; false otherwise
369      */
370     bool processReplacement(const RoomMessageEvent& newEvent);
371 
372     void setTags(TagsMap&& newTags);
373 
374     QJsonObject toJson() const;
375 
isLocalUser(const User * u) const376     bool isLocalUser(const User* u) const { return u == q->localUser(); }
377 
378 #ifdef Quotient_E2EE_ENABLED
379     // A map from <sessionId, messageIndex> to <event_id, origin_server_ts>
380     QHash<QPair<QString, uint32_t>, QPair<QString, QDateTime>>
381         groupSessionIndexRecord; // TODO: cache
382     // A map from senderKey to a map of sessionId to InboundGroupSession
383     // Not using QMultiHash, because we want to quickly return
384     // a number of relations for a given event without enumerating them.
385     QHash<QPair<QString, QString>, InboundGroupSession*> groupSessions; // TODO:
386                                                                         // cache
addInboundGroupSession(QString senderKey,QString sessionId,QString sessionKey)387     bool addInboundGroupSession(QString senderKey, QString sessionId,
388                                 QString sessionKey)
389     {
390         if (groupSessions.contains({ senderKey, sessionId })) {
391             qCDebug(E2EE) << "Inbound Megolm session" << sessionId
392                           << "with senderKey" << senderKey << "already exists";
393             return false;
394         }
395 
396         InboundGroupSession* megolmSession;
397         try {
398             megolmSession = new InboundGroupSession(sessionKey.toLatin1(),
399                                                     InboundGroupSession::Init,
400                                                     q);
401         } catch (OlmError* e) {
402             qCDebug(E2EE) << "Unable to create new InboundGroupSession"
403                           << e->what();
404             return false;
405         }
406         if (megolmSession->id() != sessionId) {
407             qCDebug(E2EE) << "Session ID mismatch in m.room_key event sent "
408                              "from sender with key"
409                           << senderKey;
410             return false;
411         }
412         groupSessions.insert({ senderKey, sessionId }, megolmSession);
413         return true;
414     }
415 
groupSessionDecryptMessage(QByteArray cipher,const QString & senderKey,const QString & sessionId,const QString & eventId,QDateTime timestamp)416     QString groupSessionDecryptMessage(QByteArray cipher,
417                                        const QString& senderKey,
418                                        const QString& sessionId,
419                                        const QString& eventId,
420                                        QDateTime timestamp)
421     {
422         std::pair<QString, uint32_t> decrypted;
423         QPair<QString, QString> senderSessionPairKey =
424             qMakePair(senderKey, sessionId);
425         if (!groupSessions.contains(senderSessionPairKey)) {
426             qCDebug(E2EE) << "Unable to decrypt event" << eventId
427                           << "The sender's device has not sent us the keys for "
428                              "this message";
429             return QString();
430         }
431         InboundGroupSession* senderSession =
432             groupSessions.value(senderSessionPairKey);
433         if (!senderSession) {
434             qCDebug(E2EE) << "Unable to decrypt event" << eventId
435                           << "senderSessionPairKey:" << senderSessionPairKey;
436             return QString();
437         }
438         try {
439             decrypted = senderSession->decrypt(cipher);
440         } catch (OlmError* e) {
441             qCDebug(E2EE) << "Unable to decrypt event" << eventId
442                           << "with matching megolm session:" << e->what();
443             return QString();
444         }
445         QPair<QString, QDateTime> properties = groupSessionIndexRecord.value(
446             qMakePair(senderSession->id(), decrypted.second));
447         if (properties.first.isEmpty()) {
448             groupSessionIndexRecord.insert(qMakePair(senderSession->id(),
449                                                      decrypted.second),
450                                            qMakePair(eventId, timestamp));
451         } else {
452             if ((properties.first != eventId)
453                 || (properties.second != timestamp)) {
454                 qCDebug(E2EE) << "Detected a replay attack on event" << eventId;
455                 return QString();
456             }
457         }
458 
459         return decrypted.first;
460     }
461 #endif // Quotient_E2EE_ENABLED
462 
463 private:
464     using users_shortlist_t = std::array<User*, 3>;
465     template <typename ContT>
466     users_shortlist_t buildShortlist(const ContT& users) const;
467     users_shortlist_t buildShortlist(const QStringList& userIds) const;
468 };
469 
470 decltype(Room::Private::baseState) Room::Private::stubbedState {};
471 
Room(Connection * connection,QString id,JoinState initialJoinState)472 Room::Room(Connection* connection, QString id, JoinState initialJoinState)
473     : QObject(connection), d(new Private(connection, id, initialJoinState))
474 {
475     setObjectName(id);
476     // See "Accessing the Public Class" section in
477     // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/
478     d->q = this;
479     d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name
480     connectUntil(connection, &Connection::loadedRoomState, this, [this](Room* r) {
481         if (this == r)
482             emit baseStateLoaded();
483         return this == r; // loadedRoomState fires only once per room
484     });
485     qCDebug(STATE) << "New" << toCString(initialJoinState) << "Room:" << id;
486 }
487 
~Room()488 Room::~Room() { delete d; }
489 
id() const490 const QString& Room::id() const { return d->id; }
491 
version() const492 QString Room::version() const
493 {
494     const auto v = d->getCurrentState<RoomCreateEvent>()->version();
495     return v.isEmpty() ? QStringLiteral("1") : v;
496 }
497 
isUnstable() const498 bool Room::isUnstable() const
499 {
500     return !connection()->loadingCapabilities()
501            && !connection()->stableRoomVersions().contains(version());
502 }
503 
predecessorId() const504 QString Room::predecessorId() const
505 {
506     return d->getCurrentState<RoomCreateEvent>()->predecessor().roomId;
507 }
508 
predecessor(JoinStates statesFilter) const509 Room* Room::predecessor(JoinStates statesFilter) const
510 {
511     if (const auto& predId = predecessorId(); !predId.isEmpty())
512         if (auto* r = connection()->room(predId, statesFilter);
513                 r && r->successorId() == id())
514             return r;
515 
516     return nullptr;
517 }
518 
successorId() const519 QString Room::successorId() const
520 {
521     return d->getCurrentState<RoomTombstoneEvent>()->successorRoomId();
522 }
523 
successor(JoinStates statesFilter) const524 Room* Room::successor(JoinStates statesFilter) const
525 {
526     if (const auto& succId = successorId(); !succId.isEmpty())
527         if (auto* r = connection()->room(succId, statesFilter);
528                 r && r->predecessorId() == id())
529             return r;
530 
531     return nullptr;
532 }
533 
messageEvents() const534 const Room::Timeline& Room::messageEvents() const { return d->timeline; }
535 
pendingEvents() const536 const Room::PendingEvents& Room::pendingEvents() const
537 {
538     return d->unsyncedEvents;
539 }
540 
allHistoryLoaded() const541 bool Room::allHistoryLoaded() const
542 {
543     return !d->timeline.empty() && is<RoomCreateEvent>(*d->timeline.front());
544 }
545 
name() const546 QString Room::name() const
547 {
548     return d->getCurrentState<RoomNameEvent>()->name();
549 }
550 
aliases() const551 QStringList Room::aliases() const
552 {
553     const auto* evt = d->getCurrentState<RoomCanonicalAliasEvent>();
554     auto result = evt->altAliases();
555     if (!evt->alias().isEmpty())
556         result << evt->alias();
557     return result;
558 }
559 
altAliases() const560 QStringList Room::altAliases() const
561 {
562     return d->getCurrentState<RoomCanonicalAliasEvent>()->altAliases();
563 }
564 
localAliases() const565 QStringList Room::localAliases() const
566 {
567     return d->getCurrentState<RoomAliasesEvent>(
568         connection()->domain())
569         ->aliases();
570 }
571 
remoteAliases() const572 QStringList Room::remoteAliases() const
573 {
574     QStringList result;
575     for (const auto& s : std::as_const(d->aliasServers))
576         result += d->getCurrentState<RoomAliasesEvent>(s)->aliases();
577     return result;
578 }
579 
canonicalAlias() const580 QString Room::canonicalAlias() const
581 {
582     return d->getCurrentState<RoomCanonicalAliasEvent>()->alias();
583 }
584 
displayName() const585 QString Room::displayName() const { return d->displayname; }
586 
refreshDisplayName()587 void Room::refreshDisplayName() { d->updateDisplayname(); }
588 
topic() const589 QString Room::topic() const
590 {
591     return d->getCurrentState<RoomTopicEvent>()->topic();
592 }
593 
avatarMediaId() const594 QString Room::avatarMediaId() const { return d->avatar.mediaId(); }
595 
avatarUrl() const596 QUrl Room::avatarUrl() const { return d->avatar.url(); }
597 
avatarObject() const598 const Avatar& Room::avatarObject() const { return d->avatar; }
599 
avatar(int dimension)600 QImage Room::avatar(int dimension) { return avatar(dimension, dimension); }
601 
avatar(int width,int height)602 QImage Room::avatar(int width, int height)
603 {
604     if (!d->avatar.url().isEmpty())
605         return d->avatar.get(connection(), width, height,
606                              [=] { emit avatarChanged(); });
607 
608     // Use the first (excluding self) user's avatar for direct chats
609     const auto dcUsers = directChatUsers();
610     for (auto* u : dcUsers)
611         if (u != localUser())
612             return u->avatar(width, height, this, [=] { emit avatarChanged(); });
613 
614     return {};
615 }
616 
user(const QString & userId) const617 User* Room::user(const QString& userId) const
618 {
619     return connection()->user(userId);
620 }
621 
memberJoinState(User * user) const622 JoinState Room::memberJoinState(User* user) const
623 {
624     return user != nullptr && d->membersMap.contains(user->name(this), user)
625                ? JoinState::Join
626                : JoinState::Leave;
627 }
628 
joinState() const629 JoinState Room::joinState() const { return d->joinState; }
630 
setJoinState(JoinState state)631 void Room::setJoinState(JoinState state)
632 {
633     JoinState oldState = d->joinState;
634     if (state == oldState)
635         return;
636     d->joinState = state;
637     qCDebug(STATE) << "Room" << id() << "changed state: " << int(oldState)
638                    << "->" << int(state);
639     emit changed(Change::JoinStateChange);
640     emit joinStateChanged(oldState, state);
641 }
642 
setLastReadReceipt(User * u,rev_iter_t newMarker,QString newEvtId)643 void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker,
644                                        QString newEvtId)
645 {
646     if (!u) {
647         Q_ASSERT(u != nullptr); // For Debug builds
648         qCCritical(MAIN) << "Empty user, skipping read receipt registration";
649         return; // For Release builds
650     }
651     if (q->memberJoinState(u) != JoinState::Join) {
652         qCWarning(EPHEMERAL)
653             << "Won't record read receipt for non-member" << u->id();
654         return;
655     }
656 
657     if (newMarker == historyEdge() && !newEvtId.isEmpty())
658         newMarker = q->findInTimeline(newEvtId);
659     if (newMarker != historyEdge()) {
660         // NB: with reverse iterators, timeline history >= sync edge
661         if (newMarker >= q->readMarker(u)) {
662             qCDebug(EPHEMERAL) << "The new read receipt for" << u->id()
663                                << "is at or behind the old one, skipping";
664             return;
665         }
666 
667         // Try to auto-promote the read marker over the user's own messages
668         // (switch to direct iterators for that).
669         const auto eagerMarker = find_if(newMarker.base(), syncEdge(),
670                                          [=](const TimelineItem& ti) {
671                                              return ti->senderId() != u->id();
672                                          })
673                                  - 1;
674         newEvtId = (*eagerMarker)->id();
675         if (eagerMarker != newMarker.base() - 1) // &*(rIt.base() - 1) === &*rIt
676             qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << u->id()
677                                << "to" << newEvtId;
678     }
679 
680     auto& storedId = lastReadEventIds[u];
681     if (storedId == newEvtId)
682         return;
683     // Finally make the change
684     eventIdReadUsers.remove(storedId, u);
685     eventIdReadUsers.insert(newEvtId, u);
686     swap(storedId, newEvtId); // Now newEvtId actually stores the old eventId
687     qCDebug(EPHEMERAL) << "The new read receipt for" << u->id() << "is at"
688                        << storedId;
689     emit q->lastReadEventChanged(u);
690     if (!isLocalUser(u))
691         emit q->readMarkerForUserMoved(u, newEvtId, storedId);
692 }
693 
updateUnreadCount(const rev_iter_t & from,const rev_iter_t & to)694 Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from,
695                                                const rev_iter_t& to)
696 {
697     Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend());
698     Q_ASSERT(to >= from && to <= timeline.crend());
699 
700     auto fullyReadMarker = q->readMarker();
701     if (fullyReadMarker < from)
702         return NoChange; // What's arrived is already fully read
703 
704     if (fullyReadMarker == historyEdge() && q->allHistoryLoaded())
705         --fullyReadMarker; // No read marker in the whole room, initialise it
706     if (fullyReadMarker < to) {
707         // Catch a special case when the last fully read event id refers to an
708         // event that has just arrived. In this case we should recalculate
709         // unreadMessages to get an exact number instead of an estimation
710         // (see https://github.com/quotient-im/libQuotient/wiki/unread_count).
711         // For the same reason (switching from the estimation to the exact
712         // number) this branch always emits unreadMessagesChanged() and returns
713         // UnreadNotifsChange, even if the estimation luckily matched the exact
714         // result.
715         return recalculateUnreadCount(true);
716     }
717 
718     // Fully read marker is somewhere beyond the most historical message from
719     // the arrived batch - add up newly arrived messages to the current counter,
720     // instead of a complete recalculation.
721     Q_ASSERT(to <= fullyReadMarker);
722 
723     QElapsedTimer et;
724     et.start();
725     const auto newUnreadMessages =
726         count_if(from, to,
727                  std::bind(&Room::Private::isEventNotable, this, _1));
728     if (et.nsecsElapsed() > profilerMinNsecs() / 10)
729         qCDebug(PROFILER) << "Counting gained unread messages in"
730                           << q->objectName() << "took" << et;
731 
732     if (newUnreadMessages == 0)
733         return NoChange;
734 
735     // See https://github.com/quotient-im/libQuotient/wiki/unread_count
736     if (unreadMessages < 0)
737         unreadMessages = 0;
738 
739     unreadMessages += newUnreadMessages;
740     qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained"
741                       << newUnreadMessages << "unread message(s),"
742                       << (q->readMarker() == historyEdge()
743                               ? "in total at least"
744                               : "in total")
745                       << unreadMessages << "unread message(s)";
746     emit q->unreadMessagesChanged(q);
747     return UnreadNotifsChange;
748 }
749 
recalculateUnreadCount(bool force)750 Room::Changes Room::Private::recalculateUnreadCount(bool force)
751 {
752     // The recalculation logic assumes that the fully read marker points at
753     // a specific position in the timeline
754     Q_ASSERT(q->readMarker() != historyEdge());
755     const auto oldUnreadCount = unreadMessages;
756     QElapsedTimer et;
757     et.start();
758     unreadMessages =
759         int(count_if(timeline.crbegin(), q->readMarker(),
760                      [this](const auto& ti) { return isEventNotable(ti); }));
761     if (et.nsecsElapsed() > profilerMinNsecs() / 10)
762         qCDebug(PROFILER) << "Recounting unread messages in" << q->objectName()
763                           << "took" << et;
764 
765     // See https://github.com/quotient-im/libQuotient/wiki/unread_count
766     if (unreadMessages == 0)
767         unreadMessages = -1;
768 
769     if (!force && unreadMessages == oldUnreadCount)
770         return NoChange;
771 
772     if (unreadMessages == -1)
773         qCDebug(MESSAGES)
774             << "Room" << displayname << "has no more unread messages";
775     else
776         qCDebug(MESSAGES) << "Room" << displayname << "still has"
777                           << unreadMessages << "unread message(s)";
778     emit q->unreadMessagesChanged(q);
779     return UnreadNotifsChange;
780 }
781 
setFullyReadMarker(const QString & eventId)782 Room::Changes Room::Private::setFullyReadMarker(const QString& eventId)
783 {
784     if (fullyReadUntilEventId == eventId)
785         return NoChange;
786 
787     const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId);
788     qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() //
789                       << "moved to" << fullyReadUntilEventId;
790     emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId);
791 
792     Changes changes = ReadMarkerChange;
793     if (const auto rm = q->readMarker(); rm != historyEdge()) {
794         // Pull read receipt if it's behind
795         if (auto rr = q->readMarker(q->localUser()); rr > rm)
796             setLastReadReceipt(q->localUser(), rm);
797 
798         changes |= recalculateUnreadCount();
799     }
800     return changes;
801 }
802 
markMessagesAsRead(const rev_iter_t & upToMarker)803 void Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker)
804 {
805     if (upToMarker < q->readMarker()) {
806         setFullyReadMarker((*upToMarker)->id());
807         // Assuming that if a read receipt was sent on a newer event, it will
808         // stay there instead of "un-reading" notifications/mentions from
809         // m.fully_read to m.read
810         connection->callApi<SetReadMarkerJob>(BackgroundRequest, id,
811                                               fullyReadUntilEventId,
812                                               fullyReadUntilEventId);
813     }
814 }
815 
markMessagesAsRead(QString uptoEventId)816 void Room::markMessagesAsRead(QString uptoEventId)
817 {
818     d->markMessagesAsRead(findInTimeline(uptoEventId));
819 }
820 
markAllMessagesAsRead()821 void Room::markAllMessagesAsRead()
822 {
823     if (!d->timeline.empty())
824         d->markMessagesAsRead(d->timeline.crbegin());
825 }
826 
canSwitchVersions() const827 bool Room::canSwitchVersions() const
828 {
829     if (!successorId().isEmpty())
830         return false; // No one can upgrade a room that's already upgraded
831 
832     if (const auto* plEvt = d->getCurrentState<RoomPowerLevelsEvent>()) {
833         const auto currentUserLevel = plEvt->powerLevelForUser(localUser()->id());
834         const auto tombstonePowerLevel =
835             plEvt->powerLevelForState("m.room.tombstone"_ls);
836         return currentUserLevel >= tombstonePowerLevel;
837     }
838     return true;
839 }
840 
hasUnreadMessages() const841 bool Room::hasUnreadMessages() const { return unreadCount() >= 0; }
842 
unreadCount() const843 int Room::unreadCount() const { return d->unreadMessages; }
844 
historyEdge() const845 Room::rev_iter_t Room::historyEdge() const { return d->historyEdge(); }
846 
syncEdge() const847 Room::Timeline::const_iterator Room::syncEdge() const { return d->syncEdge(); }
848 
timelineEdge() const849 Room::rev_iter_t Room::timelineEdge() const { return d->historyEdge(); }
850 
minTimelineIndex() const851 TimelineItem::index_t Room::minTimelineIndex() const
852 {
853     return d->timeline.empty() ? 0 : d->timeline.front().index();
854 }
855 
maxTimelineIndex() const856 TimelineItem::index_t Room::maxTimelineIndex() const
857 {
858     return d->timeline.empty() ? 0 : d->timeline.back().index();
859 }
860 
isValidIndex(TimelineItem::index_t timelineIndex) const861 bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const
862 {
863     return !d->timeline.empty() && timelineIndex >= minTimelineIndex()
864            && timelineIndex <= maxTimelineIndex();
865 }
866 
findInTimeline(TimelineItem::index_t index) const867 Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const
868 {
869     return timelineEdge()
870            - (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0);
871 }
872 
findInTimeline(const QString & evtId) const873 Room::rev_iter_t Room::findInTimeline(const QString& evtId) const
874 {
875     if (!d->timeline.empty() && d->eventsIndex.contains(evtId)) {
876         auto it = findInTimeline(d->eventsIndex.value(evtId));
877         Q_ASSERT(it != historyEdge() && (*it)->id() == evtId);
878         return it;
879     }
880     return historyEdge();
881 }
882 
findPendingEvent(const QString & txnId)883 Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId)
884 {
885     return std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
886                         [txnId](const auto& item) {
887                             return item->transactionId() == txnId;
888                         });
889 }
890 
891 Room::PendingEvents::const_iterator
findPendingEvent(const QString & txnId) const892 Room::findPendingEvent(const QString& txnId) const
893 {
894     return std::find_if(d->unsyncedEvents.cbegin(), d->unsyncedEvents.cend(),
895                         [txnId](const auto& item) {
896                             return item->transactionId() == txnId;
897                         });
898 }
899 
relatedEvents(const QString & evtId,const char * relType) const900 const Room::RelatedEvents Room::relatedEvents(const QString& evtId,
901                                               const char* relType) const
902 {
903     return d->relations.value({ evtId, relType });
904 }
905 
relatedEvents(const RoomEvent & evt,const char * relType) const906 const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt,
907                                               const char* relType) const
908 {
909     return relatedEvents(evt.id(), relType);
910 }
911 
getAllMembers()912 void Room::Private::getAllMembers()
913 {
914     // If already loaded or already loading, there's nothing to do here.
915     if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob))
916         return;
917 
918     allMembersJob = connection->callApi<GetMembersByRoomJob>(
919         id, connection->nextBatchToken(), "join");
920     auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1;
921     connect(allMembersJob, &BaseJob::success, q, [=] {
922         Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1);
923         auto roomChanges = updateStateFrom(allMembersJob->chunk());
924         // Replay member events that arrived after the point for which
925         // the full members list was requested.
926         if (!timeline.empty())
927             for (auto it = q->findInTimeline(nextIndex).base();
928                  it != syncEdge(); ++it)
929                 if (is<RoomMemberEvent>(**it))
930                     roomChanges |= q->processStateEvent(**it);
931         if (roomChanges & MembersChange)
932             emit q->memberListChanged();
933         emit q->allMembersLoaded();
934     });
935 }
936 
displayed() const937 bool Room::displayed() const { return d->displayed; }
938 
setDisplayed(bool displayed)939 void Room::setDisplayed(bool displayed)
940 {
941     if (d->displayed == displayed)
942         return;
943 
944     d->displayed = displayed;
945     emit displayedChanged(displayed);
946     if (displayed)
947         d->getAllMembers();
948 }
949 
firstDisplayedEventId() const950 QString Room::firstDisplayedEventId() const { return d->firstDisplayedEventId; }
951 
firstDisplayedMarker() const952 Room::rev_iter_t Room::firstDisplayedMarker() const
953 {
954     return findInTimeline(firstDisplayedEventId());
955 }
956 
setFirstDisplayedEventId(const QString & eventId)957 void Room::setFirstDisplayedEventId(const QString& eventId)
958 {
959     if (d->firstDisplayedEventId == eventId)
960         return;
961 
962     if (!eventId.isEmpty() && findInTimeline(eventId) == historyEdge())
963         qCWarning(MESSAGES)
964             << eventId
965             << "is marked as first displayed but doesn't seem to be loaded";
966 
967     d->firstDisplayedEventId = eventId;
968     emit firstDisplayedEventChanged();
969 }
970 
setFirstDisplayedEvent(TimelineItem::index_t index)971 void Room::setFirstDisplayedEvent(TimelineItem::index_t index)
972 {
973     Q_ASSERT(isValidIndex(index));
974     setFirstDisplayedEventId(findInTimeline(index)->event()->id());
975 }
976 
lastDisplayedEventId() const977 QString Room::lastDisplayedEventId() const { return d->lastDisplayedEventId; }
978 
lastDisplayedMarker() const979 Room::rev_iter_t Room::lastDisplayedMarker() const
980 {
981     return findInTimeline(lastDisplayedEventId());
982 }
983 
setLastDisplayedEventId(const QString & eventId)984 void Room::setLastDisplayedEventId(const QString& eventId)
985 {
986     if (d->lastDisplayedEventId == eventId)
987         return;
988 
989     const auto marker = findInTimeline(eventId);
990     if (!eventId.isEmpty() && marker == historyEdge())
991         qCWarning(MESSAGES)
992             << eventId
993             << "is marked as last displayed but doesn't seem to be loaded";
994 
995     d->lastDisplayedEventId = eventId;
996     emit lastDisplayedEventChanged();
997     if (d->displayed && marker < readMarker(localUser())) {
998         d->setLastReadReceipt(localUser(), marker);
999         connection()->callApi<PostReceiptJob>(BackgroundRequest, id(),
1000                                               QStringLiteral("m.read"),
1001                                               QUrl::toPercentEncoding(eventId));
1002     }
1003 }
1004 
setLastDisplayedEvent(TimelineItem::index_t index)1005 void Room::setLastDisplayedEvent(TimelineItem::index_t index)
1006 {
1007     Q_ASSERT(isValidIndex(index));
1008     setLastDisplayedEventId(findInTimeline(index)->event()->id());
1009 }
1010 
readMarker(const User * user) const1011 Room::rev_iter_t Room::readMarker(const User* user) const
1012 {
1013     Q_ASSERT(user);
1014     return findInTimeline(d->lastReadEventIds.value(user));
1015 }
1016 
readMarker() const1017 Room::rev_iter_t Room::readMarker() const
1018 {
1019     return findInTimeline(d->fullyReadUntilEventId);
1020 }
1021 
readMarkerEventId() const1022 QString Room::readMarkerEventId() const
1023 {
1024     return d->fullyReadUntilEventId;
1025 }
1026 
usersAtEventId(const QString & eventId)1027 QList<User*> Room::usersAtEventId(const QString& eventId)
1028 {
1029     return d->eventIdReadUsers.values(eventId);
1030 }
1031 
notificationCount() const1032 int Room::notificationCount() const { return d->notificationCount; }
1033 
resetNotificationCount()1034 void Room::resetNotificationCount()
1035 {
1036     if (d->notificationCount == 0)
1037         return;
1038     d->notificationCount = 0;
1039     emit notificationCountChanged();
1040 }
1041 
highlightCount() const1042 int Room::highlightCount() const { return d->highlightCount; }
1043 
resetHighlightCount()1044 void Room::resetHighlightCount()
1045 {
1046     if (d->highlightCount == 0)
1047         return;
1048     d->highlightCount = 0;
1049     emit highlightCountChanged();
1050 }
1051 
switchVersion(QString newVersion)1052 void Room::switchVersion(QString newVersion)
1053 {
1054     if (!successorId().isEmpty()) {
1055         Q_ASSERT(!successorId().isEmpty());
1056         emit upgradeFailed(tr("The room is already upgraded"));
1057     }
1058     if (auto* job = connection()->callApi<UpgradeRoomJob>(id(), newVersion))
1059         connect(job, &BaseJob::failure, this,
1060                 [this, job] { emit upgradeFailed(job->errorString()); });
1061     else
1062         emit upgradeFailed(tr("Couldn't initiate upgrade"));
1063 }
1064 
hasAccountData(const QString & type) const1065 bool Room::hasAccountData(const QString& type) const
1066 {
1067     return d->accountData.find(type) != d->accountData.end();
1068 }
1069 
accountData(const QString & type) const1070 const EventPtr& Room::accountData(const QString& type) const
1071 {
1072     static EventPtr NoEventPtr {};
1073     const auto it = d->accountData.find(type);
1074     return it != d->accountData.end() ? it->second : NoEventPtr;
1075 }
1076 
tagNames() const1077 QStringList Room::tagNames() const { return d->tags.keys(); }
1078 
tags() const1079 TagsMap Room::tags() const { return d->tags; }
1080 
tag(const QString & name) const1081 TagRecord Room::tag(const QString& name) const { return d->tags.value(name); }
1082 
validatedTag(QString name)1083 std::pair<bool, QString> validatedTag(QString name)
1084 {
1085     if (name.isEmpty() || name.indexOf('.', 1) != -1)
1086         return { false, name };
1087 
1088     qCWarning(MAIN) << "The tag" << name
1089                     << "doesn't follow the CS API conventions";
1090     name.prepend("u.");
1091     qCWarning(MAIN) << "Using " << name << "instead";
1092 
1093     return { true, name };
1094 }
1095 
addTag(const QString & name,const TagRecord & record)1096 void Room::addTag(const QString& name, const TagRecord& record)
1097 {
1098     const auto& checkRes = validatedTag(name);
1099     if (d->tags.contains(name)
1100         || (checkRes.first && d->tags.contains(checkRes.second)))
1101         return;
1102 
1103     emit tagsAboutToChange();
1104     d->tags.insert(checkRes.second, record);
1105     emit tagsChanged();
1106     connection()->callApi<SetRoomTagJob>(localUser()->id(), id(),
1107                                          checkRes.second, record.order);
1108 }
1109 
addTag(const QString & name,float order)1110 void Room::addTag(const QString& name, float order)
1111 {
1112     addTag(name, TagRecord { order });
1113 }
1114 
removeTag(const QString & name)1115 void Room::removeTag(const QString& name)
1116 {
1117     if (d->tags.contains(name)) {
1118         emit tagsAboutToChange();
1119         d->tags.remove(name);
1120         emit tagsChanged();
1121         connection()->callApi<DeleteRoomTagJob>(localUser()->id(), id(), name);
1122     } else if (!name.startsWith("u."))
1123         removeTag("u." + name);
1124     else
1125         qCWarning(MAIN) << "Tag" << name << "on room" << objectName()
1126                        << "not found, nothing to remove";
1127 }
1128 
setTags(TagsMap newTags,ActionScope applyOn)1129 void Room::setTags(TagsMap newTags, ActionScope applyOn)
1130 {
1131     bool propagate = applyOn != ActionScope::ThisRoomOnly;
1132     auto joinStates =
1133         applyOn == ActionScope::WithinSameState ? joinState() :
1134         applyOn == ActionScope::OmitLeftState ? JoinState::Join|JoinState::Invite :
1135         JoinState::Join|JoinState::Invite|JoinState::Leave;
1136     if (propagate) {
1137         for (auto* r = this; (r = r->predecessor(joinStates));)
1138             r->setTags(newTags, ActionScope::ThisRoomOnly);
1139     }
1140 
1141     d->setTags(move(newTags));
1142     connection()->callApi<SetAccountDataPerRoomJob>(
1143         localUser()->id(), id(), TagEvent::matrixTypeId(),
1144         TagEvent(d->tags).contentJson());
1145 
1146     if (propagate) {
1147         for (auto* r = this; (r = r->successor(joinStates));)
1148             r->setTags(d->tags, ActionScope::ThisRoomOnly);
1149     }
1150 }
1151 
setTags(TagsMap && newTags)1152 void Room::Private::setTags(TagsMap&& newTags)
1153 {
1154     emit q->tagsAboutToChange();
1155     const auto keys = newTags.keys();
1156     for (const auto& k : keys)
1157         if (const auto& [adjusted, adjustedTag] = validatedTag(k); adjusted) {
1158             if (newTags.contains(adjustedTag))
1159                 newTags.remove(k);
1160             else
1161                 newTags.insert(adjustedTag, newTags.take(k));
1162         }
1163 
1164     tags = move(newTags);
1165     qCDebug(STATE) << "Room" << q->objectName() << "is tagged with"
1166                    << q->tagNames().join(QStringLiteral(", "));
1167     emit q->tagsChanged();
1168 }
1169 
isFavourite() const1170 bool Room::isFavourite() const { return d->tags.contains(FavouriteTag); }
1171 
isLowPriority() const1172 bool Room::isLowPriority() const { return d->tags.contains(LowPriorityTag); }
1173 
isServerNoticeRoom() const1174 bool Room::isServerNoticeRoom() const
1175 {
1176     return d->tags.contains(ServerNoticeTag);
1177 }
1178 
isDirectChat() const1179 bool Room::isDirectChat() const { return connection()->isDirectChat(id()); }
1180 
directChatUsers() const1181 QList<User*> Room::directChatUsers() const
1182 {
1183     return connection()->directChatUsers(this);
1184 }
1185 
safeFileName(QString rawName)1186 QString safeFileName(QString rawName)
1187 {
1188     return rawName.replace(QRegularExpression("[/\\<>|\"*?:]"), "_");
1189 }
1190 
1191 const RoomMessageEvent*
getEventWithFile(const QString & eventId) const1192 Room::Private::getEventWithFile(const QString& eventId) const
1193 {
1194     auto evtIt = q->findInTimeline(eventId);
1195     if (evtIt != timeline.rend() && is<RoomMessageEvent>(**evtIt)) {
1196         auto* event = evtIt->viewAs<RoomMessageEvent>();
1197         if (event->hasFileContent())
1198             return event;
1199     }
1200     qCWarning(MAIN) << "No files to download in event" << eventId;
1201     return nullptr;
1202 }
1203 
fileNameToDownload(const RoomMessageEvent * event) const1204 QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const
1205 {
1206     Q_ASSERT(event && event->hasFileContent());
1207     const auto* fileInfo = event->content()->fileInfo();
1208     QString fileName;
1209     if (!fileInfo->originalName.isEmpty())
1210         fileName = QFileInfo(safeFileName(fileInfo->originalName)).fileName();
1211     else if (QUrl u { event->plainBody() }; u.isValid()) {
1212         qDebug(MAIN) << event->id()
1213                      << "has no file name supplied but the event body "
1214                         "looks like a URL - using the file name from it";
1215         fileName = u.fileName();
1216     }
1217     if (fileName.isEmpty())
1218         return safeFileName(fileInfo->mediaId()).replace('.', '-') % '.'
1219                % fileInfo->mimeType.preferredSuffix();
1220 
1221     if (QSysInfo::productType() == "windows") {
1222         if (const auto& suffixes = fileInfo->mimeType.suffixes();
1223             !suffixes.isEmpty()
1224             && std::none_of(suffixes.begin(), suffixes.end(),
1225                             [&fileName](const QString& s) {
1226                                 return fileName.endsWith(s);
1227                             }))
1228             return fileName % '.' % fileInfo->mimeType.preferredSuffix();
1229     }
1230     return fileName;
1231 }
1232 
urlToThumbnail(const QString & eventId) const1233 QUrl Room::urlToThumbnail(const QString& eventId) const
1234 {
1235     if (auto* event = d->getEventWithFile(eventId))
1236         if (event->hasThumbnail()) {
1237             auto* thumbnail = event->content()->thumbnailInfo();
1238             Q_ASSERT(thumbnail != nullptr);
1239             return MediaThumbnailJob::makeRequestUrl(connection()->homeserver(),
1240                                                      thumbnail->url,
1241                                                      thumbnail->imageSize);
1242         }
1243     qCDebug(MAIN) << "Event" << eventId << "has no thumbnail";
1244     return {};
1245 }
1246 
urlToDownload(const QString & eventId) const1247 QUrl Room::urlToDownload(const QString& eventId) const
1248 {
1249     if (auto* event = d->getEventWithFile(eventId)) {
1250         auto* fileInfo = event->content()->fileInfo();
1251         Q_ASSERT(fileInfo != nullptr);
1252         return DownloadFileJob::makeRequestUrl(connection()->homeserver(),
1253                                                fileInfo->url);
1254     }
1255     return {};
1256 }
1257 
fileNameToDownload(const QString & eventId) const1258 QString Room::fileNameToDownload(const QString& eventId) const
1259 {
1260     if (auto* event = d->getEventWithFile(eventId))
1261         return d->fileNameToDownload(event);
1262     return {};
1263 }
1264 
fileTransferInfo(const QString & id) const1265 FileTransferInfo Room::fileTransferInfo(const QString& id) const
1266 {
1267     const auto infoIt = d->fileTransfers.constFind(id);
1268     if (infoIt == d->fileTransfers.cend())
1269         return {};
1270 
1271     // FIXME: Add lib tests to make sure FileTransferInfo::status stays
1272     // consistent with FileTransferInfo::job
1273 
1274     qint64 progress = infoIt->progress;
1275     qint64 total = infoIt->total;
1276     if (total > INT_MAX) {
1277         // JavaScript doesn't deal with 64-bit integers; scale down if necessary
1278         progress = llround(double(progress) / total * INT_MAX);
1279         total = INT_MAX;
1280     }
1281 
1282     return { infoIt->status,
1283              infoIt->isUpload,
1284              int(progress),
1285              int(total),
1286              QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()),
1287              QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()) };
1288 }
1289 
fileSource(const QString & id) const1290 QUrl Room::fileSource(const QString& id) const
1291 {
1292     auto url = urlToDownload(id);
1293     if (url.isValid())
1294         return url;
1295 
1296     // No urlToDownload means it's a pending or completed upload.
1297     auto infoIt = d->fileTransfers.constFind(id);
1298     if (infoIt != d->fileTransfers.cend())
1299         return QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath());
1300 
1301     qCWarning(MAIN) << "File source for identifier" << id << "not found";
1302     return {};
1303 }
1304 
prettyPrint(const QString & plainText) const1305 QString Room::prettyPrint(const QString& plainText) const
1306 {
1307     return Quotient::prettyPrint(plainText);
1308 }
1309 
usersTyping() const1310 QList<User*> Room::usersTyping() const { return d->usersTyping; }
1311 
membersLeft() const1312 QList<User*> Room::membersLeft() const { return d->membersLeft; }
1313 
users() const1314 QList<User*> Room::users() const { return d->membersMap.values(); }
1315 
memberNames() const1316 QStringList Room::memberNames() const
1317 {
1318     QStringList res;
1319     res.reserve(d->membersMap.size());
1320     for (auto u : qAsConst(d->membersMap))
1321         res.append(roomMembername(u));
1322 
1323     return res;
1324 }
1325 
memberCount() const1326 int Room::memberCount() const { return d->membersMap.size(); }
1327 
timelineSize() const1328 int Room::timelineSize() const { return int(d->timeline.size()); }
1329 
usesEncryption() const1330 bool Room::usesEncryption() const
1331 {
1332     return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty();
1333 }
1334 
getCurrentState(const QString & evtType,const QString & stateKey) const1335 const StateEventBase* Room::getCurrentState(const QString& evtType,
1336                                             const QString& stateKey) const
1337 {
1338     return d->getCurrentState({ evtType, stateKey });
1339 }
1340 
decryptMessage(const EncryptedEvent & encryptedEvent)1341 RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent)
1342 {
1343 #ifndef Quotient_E2EE_ENABLED
1344     Q_UNUSED(encryptedEvent)
1345     qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
1346     return {};
1347 #else // Quotient_E2EE_ENABLED
1348     if (encryptedEvent.algorithm() == MegolmV1AesSha2AlgoKey) {
1349         QString decrypted = d->groupSessionDecryptMessage(
1350             encryptedEvent.ciphertext(), encryptedEvent.senderKey(),
1351             encryptedEvent.sessionId(), encryptedEvent.id(),
1352             encryptedEvent.originTimestamp());
1353         if (decrypted.isEmpty()) {
1354             return {};
1355         }
1356         return makeEvent<RoomMessageEvent>(
1357             QJsonDocument::fromJson(decrypted.toUtf8()).object());
1358     }
1359     qCDebug(E2EE) << "Algorithm of the encrypted event with id"
1360                   << encryptedEvent.id() << "is not for the current device";
1361     return {};
1362 #endif // Quotient_E2EE_ENABLED
1363 }
1364 
handleRoomKeyEvent(const RoomKeyEvent & roomKeyEvent,const QString & senderKey)1365 void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent,
1366                               const QString& senderKey)
1367 {
1368 #ifndef Quotient_E2EE_ENABLED
1369     Q_UNUSED(roomKeyEvent)
1370     Q_UNUSED(senderKey)
1371     qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
1372 #else // Quotient_E2EE_ENABLED
1373     if (roomKeyEvent.algorithm() != MegolmV1AesSha2AlgoKey) {
1374         qCWarning(E2EE) << "Ignoring unsupported algorithm"
1375                         << roomKeyEvent.algorithm() << "in m.room_key event";
1376     }
1377     if (d->addInboundGroupSession(senderKey, roomKeyEvent.sessionId(),
1378                                   roomKeyEvent.sessionKey())) {
1379         qCDebug(E2EE) << "added new inboundGroupSession:"
1380                       << d->groupSessions.count();
1381     }
1382 #endif // Quotient_E2EE_ENABLED
1383 }
1384 
joinedCount() const1385 int Room::joinedCount() const
1386 {
1387     return d->summary.joinedMemberCount.value_or(d->membersMap.size());
1388 }
1389 
invitedCount() const1390 int Room::invitedCount() const
1391 {
1392     // TODO: Store invited users in Room too
1393     Q_ASSERT(d->summary.invitedMemberCount.has_value());
1394     return d->summary.invitedMemberCount.value_or(0);
1395 }
1396 
totalMemberCount() const1397 int Room::totalMemberCount() const { return joinedCount() + invitedCount(); }
1398 
eventsHistoryJob() const1399 GetRoomEventsJob* Room::eventsHistoryJob() const { return d->eventsHistoryJob; }
1400 
setSummary(RoomSummary && newSummary)1401 Room::Changes Room::Private::setSummary(RoomSummary&& newSummary)
1402 {
1403     if (!summary.merge(newSummary))
1404         return Change::NoChange;
1405     qCDebug(STATE).nospace().noquote()
1406         << "Updated room summary for " << q->objectName() << ": " << summary;
1407     emit q->memberListChanged();
1408     return Change::SummaryChange;
1409 }
1410 
insertMemberIntoMap(User * u)1411 void Room::Private::insertMemberIntoMap(User* u)
1412 {
1413     const auto userName =
1414         getCurrentState<RoomMemberEvent>(u->id())->displayName();
1415     // If there is exactly one namesake of the added user, signal member
1416     // renaming for that other one because the two should be disambiguated now.
1417     const auto namesakes = membersMap.values(userName);
1418 
1419     // Callers should check they are not adding an existing user once more.
1420     Q_ASSERT(!namesakes.contains(u));
1421     if (namesakes.contains(u)) { // Release version whines but continues
1422         qCCritical(STATE) << "Trying to add a user" << u->id() << "to room"
1423                           << q->objectName() << "but that's already in it";
1424         return;
1425     }
1426 
1427     if (namesakes.size() == 1)
1428         emit q->memberAboutToRename(namesakes.front(),
1429                                     namesakes.front()->fullName(q));
1430     membersMap.insert(userName, u);
1431     if (namesakes.size() == 1)
1432         emit q->memberRenamed(namesakes.front());
1433 }
1434 
removeMemberFromMap(User * u)1435 void Room::Private::removeMemberFromMap(User* u)
1436 {
1437     const auto userName =
1438         getCurrentState<RoomMemberEvent>(u->id())->displayName();
1439 
1440     User* namesake = nullptr;
1441     auto namesakes = membersMap.values(userName);
1442     if (namesakes.size() == 2) {
1443         namesake = namesakes.front() == u ? namesakes.back() : namesakes.front();
1444         Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken");
1445         emit q->memberAboutToRename(namesake, userName);
1446     }
1447     membersMap.remove(userName, u);
1448     // If there was one namesake besides the removed user, signal member
1449     // renaming for it because it doesn't need to be disambiguated any more.
1450     if (namesake)
1451         emit q->memberRenamed(namesake);
1452 }
1453 
makeErrorStr(const Event & e,QByteArray msg)1454 inline auto makeErrorStr(const Event& e, QByteArray msg)
1455 {
1456     return msg.append("; event dump follows:\n").append(e.originalJson());
1457 }
1458 
1459 Room::Timeline::size_type
moveEventsToTimeline(RoomEventsRange events,EventsPlacement placement)1460 Room::Private::moveEventsToTimeline(RoomEventsRange events,
1461                                     EventsPlacement placement)
1462 {
1463     Q_ASSERT(!events.empty());
1464     // Historical messages arrive in newest-to-oldest order, so the process for
1465     // them is almost symmetric to the one for new messages. New messages get
1466     // appended from index 0; old messages go backwards from index -1.
1467     auto index = timeline.empty()
1468                      ? -((placement + 1) / 2) /* 1 -> -1; -1 -> 0 */
1469                      : placement == Older ? timeline.front().index()
1470                                           : timeline.back().index();
1471     auto baseIndex = index;
1472     for (auto&& e : events) {
1473         const auto eId = e->id();
1474         Q_ASSERT_X(e, __FUNCTION__, "Attempt to add nullptr to timeline");
1475         Q_ASSERT_X(
1476             !eId.isEmpty(), __FUNCTION__,
1477             makeErrorStr(*e, "Event with empty id cannot be in the timeline"));
1478         Q_ASSERT_X(
1479             !eventsIndex.contains(eId), __FUNCTION__,
1480             makeErrorStr(*e, "Event is already in the timeline; "
1481                              "incoming events were not properly deduplicated"));
1482         if (placement == Older)
1483             timeline.emplace_front(move(e), --index);
1484         else
1485             timeline.emplace_back(move(e), ++index);
1486         eventsIndex.insert(eId, index);
1487         Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId);
1488     }
1489     const auto insertedSize = (index - baseIndex) * placement;
1490     Q_ASSERT(insertedSize == int(events.size()));
1491     return Timeline::size_type(insertedSize);
1492 }
1493 
roomMembername(const User * u) const1494 QString Room::roomMembername(const User* u) const
1495 {
1496     // See the CS spec, section 11.2.2.3
1497 
1498     const auto username = u->name(this);
1499     if (username.isEmpty())
1500         return u->id();
1501 
1502     auto namesakesIt = qAsConst(d->membersMap).find(username);
1503 
1504     // We expect a user to be a member of the room - but technically it is
1505     // possible to invoke roomMemberName() even for non-members. In such case
1506     // we return the full name, just in case.
1507     if (namesakesIt == d->membersMap.cend())
1508         return u->fullName(this);
1509 
1510     auto nextUserIt = namesakesIt;
1511     if (++nextUserIt == d->membersMap.cend() || nextUserIt.key() != username)
1512         return username; // No disambiguation necessary
1513 
1514     return u->fullName(this); // Disambiguate fully
1515 }
1516 
roomMembername(const QString & userId) const1517 QString Room::roomMembername(const QString& userId) const
1518 {
1519     if (auto* const u = user(userId))
1520         return roomMembername(u);
1521     return {};
1522 }
1523 
safeMemberName(const QString & userId) const1524 QString Room::safeMemberName(const QString& userId) const
1525 {
1526     return sanitized(roomMembername(userId));
1527 }
1528 
updateData(SyncRoomData && data,bool fromCache)1529 void Room::updateData(SyncRoomData&& data, bool fromCache)
1530 {
1531     if (d->prevBatch.isEmpty())
1532         d->prevBatch = data.timelinePrevBatch;
1533     setJoinState(data.joinState);
1534 
1535     Changes roomChanges = Change::NoChange;
1536     for (auto&& event : data.accountData)
1537         roomChanges |= processAccountDataEvent(move(event));
1538 
1539     roomChanges |= d->updateStateFrom(data.state);
1540     // The order of calculation is important - don't merge these lines!
1541     roomChanges |= d->addNewMessageEvents(move(data.timeline));
1542 
1543     if (roomChanges & TopicChange)
1544         emit topicChanged();
1545 
1546     if (roomChanges & (NameChange | AliasesChange))
1547         emit namesChanged(this);
1548 
1549     if (roomChanges & MembersChange)
1550         emit memberListChanged();
1551 
1552     roomChanges |= d->setSummary(move(data.summary));
1553 
1554     for (auto&& ephemeralEvent : data.ephemeral)
1555         roomChanges |= processEphemeralEvent(move(ephemeralEvent));
1556 
1557     // See https://github.com/quotient-im/libQuotient/wiki/unread_count
1558     // -2 is a special value to which SyncRoomData::SyncRoomData sets
1559     // unreadCount when it's missing in the payload (to distinguish from
1560     // explicit 0 in the payload).
1561     if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) {
1562         qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount;
1563         d->unreadMessages = data.unreadCount;
1564         emit unreadMessagesChanged(this);
1565     }
1566 
1567     // Similar to unreadCount, SyncRoomData constructor assigns -1 to
1568     // highlightCount/notificationCount when those are missing in the payload
1569     if (data.highlightCount != -1 && data.highlightCount != d->highlightCount) {
1570         qCDebug(MESSAGES).nospace()
1571             << "Highlights in " << objectName() //
1572             << ": " << d->highlightCount << " -> " << data.highlightCount;
1573         d->highlightCount = data.highlightCount;
1574         emit highlightCountChanged();
1575     }
1576     if (data.notificationCount != -1
1577         && data.notificationCount != d->notificationCount) //
1578     {
1579         qCDebug(MESSAGES).nospace()
1580             << "Notifications in " << objectName() //
1581             << ": " << d->notificationCount << " -> " << data.notificationCount;
1582         d->notificationCount = data.notificationCount;
1583         emit notificationCountChanged();
1584     }
1585     if (roomChanges != Change::NoChange) {
1586         d->updateDisplayname();
1587         emit changed(roomChanges);
1588         if (!fromCache)
1589             connection()->saveRoomState(this);
1590     }
1591 }
1592 
addAsPending(RoomEventPtr && event)1593 RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event)
1594 {
1595     if (event->transactionId().isEmpty())
1596         event->setTransactionId(connection->generateTxnId());
1597     if (event->roomId().isEmpty())
1598         event->setRoomId(id);
1599     if (event->senderId().isEmpty())
1600         event->setSender(connection->userId());
1601     auto* pEvent = rawPtr(event);
1602     emit q->pendingEventAboutToAdd(pEvent);
1603     unsyncedEvents.emplace_back(move(event));
1604     emit q->pendingEventAdded();
1605     return pEvent;
1606 }
1607 
sendEvent(RoomEventPtr && event)1608 QString Room::Private::sendEvent(RoomEventPtr&& event)
1609 {
1610     if (q->usesEncryption()) {
1611         qCCritical(MAIN) << "Room" << q->objectName()
1612                          << "enforces encryption; sending encrypted messages "
1613                             "is not supported yet";
1614     }
1615     if (q->successorId().isEmpty())
1616         return doSendEvent(addAsPending(std::move(event)));
1617 
1618     qCWarning(MAIN) << q << "has been upgraded, event won't be sent";
1619     return {};
1620 }
1621 
doSendEvent(const RoomEvent * pEvent)1622 QString Room::Private::doSendEvent(const RoomEvent* pEvent)
1623 {
1624     const auto txnId = pEvent->transactionId();
1625     // TODO, #133: Enqueue the job rather than immediately trigger it.
1626     if (auto call =
1627             connection->callApi<SendMessageJob>(BackgroundRequest, id,
1628                                                 pEvent->matrixType(), txnId,
1629                                                 pEvent->contentJson())) {
1630         Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] {
1631             auto it = q->findPendingEvent(txnId);
1632             if (it == unsyncedEvents.end()) {
1633                 qCWarning(EVENTS) << "Pending event for transaction" << txnId
1634                                  << "not found - got synced so soon?";
1635                 return;
1636             }
1637             it->setDeparted();
1638             qCDebug(EVENTS) << "Event txn" << txnId << "has departed";
1639             emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
1640         });
1641         Room::connect(call, &BaseJob::failure, q,
1642                       std::bind(&Room::Private::onEventSendingFailure, this,
1643                                 txnId, call));
1644         Room::connect(call, &BaseJob::success, q, [this, call, txnId] {
1645             auto it = q->findPendingEvent(txnId);
1646             if (it != unsyncedEvents.end()) {
1647                 if (it->deliveryStatus() != EventStatus::ReachedServer) {
1648                     it->setReachedServer(call->eventId());
1649                     emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
1650                 }
1651             } else
1652                 qCDebug(EVENTS) << "Pending event for transaction" << txnId
1653                                << "already merged";
1654 
1655             emit q->messageSent(txnId, call->eventId());
1656         });
1657     } else
1658         onEventSendingFailure(txnId);
1659     return txnId;
1660 }
1661 
onEventSendingFailure(const QString & txnId,BaseJob * call)1662 void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call)
1663 {
1664     auto it = q->findPendingEvent(txnId);
1665     if (it == unsyncedEvents.end()) {
1666         qCritical(EVENTS) << "Pending event for transaction" << txnId
1667                           << "could not be sent";
1668         return;
1669     }
1670     it->setSendingFailed(call ? call->statusCaption() % ": " % call->errorString()
1671                               : tr("The call could not be started"));
1672     emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
1673 }
1674 
retryMessage(const QString & txnId)1675 QString Room::retryMessage(const QString& txnId)
1676 {
1677     const auto it = findPendingEvent(txnId);
1678     Q_ASSERT(it != d->unsyncedEvents.end());
1679     qCDebug(EVENTS) << "Retrying transaction" << txnId;
1680     const auto& transferIt = d->fileTransfers.constFind(txnId);
1681     if (transferIt != d->fileTransfers.cend()) {
1682         Q_ASSERT(transferIt->isUpload);
1683         if (transferIt->status == FileTransferInfo::Completed) {
1684             qCDebug(MESSAGES)
1685                 << "File for transaction" << txnId
1686                 << "has already been uploaded, bypassing re-upload";
1687         } else {
1688             if (isJobRunning(transferIt->job)) {
1689                 qCDebug(MESSAGES) << "Abandoning the upload job for transaction"
1690                                   << txnId << "and starting again";
1691                 transferIt->job->abandon();
1692                 emit fileTransferFailed(txnId,
1693                                         tr("File upload will be retried"));
1694             }
1695             uploadFile(txnId, QUrl::fromLocalFile(
1696                                   transferIt->localFileInfo.absoluteFilePath()));
1697             // FIXME: Content type is no more passed here but it should
1698         }
1699     }
1700     if (it->deliveryStatus() == EventStatus::ReachedServer) {
1701         qCWarning(MAIN)
1702             << "The previous attempt has reached the server; two"
1703                " events are likely to be in the timeline after retry";
1704     }
1705     it->resetStatus();
1706     emit pendingEventChanged(int(it - d->unsyncedEvents.begin()));
1707     return d->doSendEvent(it->event());
1708 }
1709 
discardMessage(const QString & txnId)1710 void Room::discardMessage(const QString& txnId)
1711 {
1712     auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
1713                            [txnId](const auto& evt) {
1714                                return evt->transactionId() == txnId;
1715                            });
1716     Q_ASSERT(it != d->unsyncedEvents.end());
1717     qCDebug(EVENTS) << "Discarding transaction" << txnId;
1718     const auto& transferIt = d->fileTransfers.find(txnId);
1719     if (transferIt != d->fileTransfers.end()) {
1720         Q_ASSERT(transferIt->isUpload);
1721         if (isJobRunning(transferIt->job)) {
1722             transferIt->status = FileTransferInfo::Cancelled;
1723             transferIt->job->abandon();
1724             emit fileTransferFailed(txnId, tr("File upload cancelled"));
1725         } else if (transferIt->status == FileTransferInfo::Completed) {
1726             qCWarning(MAIN)
1727                 << "File for transaction" << txnId
1728                 << "has been uploaded but the message was discarded";
1729         }
1730     }
1731     emit pendingEventAboutToDiscard(int(it - d->unsyncedEvents.begin()));
1732     d->unsyncedEvents.erase(it);
1733     emit pendingEventDiscarded();
1734 }
1735 
postMessage(const QString & plainText,MessageEventType type)1736 QString Room::postMessage(const QString& plainText, MessageEventType type)
1737 {
1738     return d->sendEvent<RoomMessageEvent>(plainText, type);
1739 }
1740 
postPlainText(const QString & plainText)1741 QString Room::postPlainText(const QString& plainText)
1742 {
1743     return postMessage(plainText, MessageEventType::Text);
1744 }
1745 
postHtmlMessage(const QString & plainText,const QString & html,MessageEventType type)1746 QString Room::postHtmlMessage(const QString& plainText, const QString& html,
1747                               MessageEventType type)
1748 {
1749     return d->sendEvent<RoomMessageEvent>(
1750         plainText, type,
1751         new EventContent::TextContent(html, QStringLiteral("text/html")));
1752 }
1753 
postHtmlText(const QString & plainText,const QString & html)1754 QString Room::postHtmlText(const QString& plainText, const QString& html)
1755 {
1756     return postHtmlMessage(plainText, html);
1757 }
1758 
postReaction(const QString & eventId,const QString & key)1759 QString Room::postReaction(const QString& eventId, const QString& key)
1760 {
1761     return d->sendEvent<ReactionEvent>(EventRelation::annotate(eventId, key));
1762 }
1763 
postFile(const QString & plainText,const QUrl & localPath,bool asGenericFile)1764 QString Room::postFile(const QString& plainText, const QUrl& localPath,
1765                        bool asGenericFile)
1766 {
1767     QFileInfo localFile { localPath.toLocalFile() };
1768     Q_ASSERT(localFile.isFile());
1769 
1770     const auto txnId =
1771         d->addAsPending(
1772              makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile))
1773             ->transactionId();
1774     // Remote URL will only be known after upload; fill in the local path
1775     // to enable the preview while the event is pending.
1776     uploadFile(txnId, localPath);
1777     // Below, the upload job is used as a context object to clean up connections
1778     const auto& transferJob = d->fileTransfers.value(txnId).job;
1779     connect(this, &Room::fileTransferCompleted, transferJob,
1780             [this, txnId](const QString& id, const QUrl&, const QUrl& mxcUri) {
1781                 if (id == txnId) {
1782                     auto it = findPendingEvent(txnId);
1783                     if (it != d->unsyncedEvents.end()) {
1784                         it->setFileUploaded(mxcUri);
1785                         emit pendingEventChanged(
1786                             int(it - d->unsyncedEvents.begin()));
1787                         d->doSendEvent(it->get());
1788                     } else {
1789                         // Normally in this situation we should instruct
1790                         // the media server to delete the file; alas, there's no
1791                         // API specced for that.
1792                         qCWarning(MAIN) << "File uploaded to" << mxcUri
1793                                         << "but the event referring to it was "
1794                                            "cancelled";
1795                     }
1796                 }
1797             });
1798     connect(this, &Room::fileTransferCancelled, transferJob,
1799             [this, txnId](const QString& id) {
1800                 if (id == txnId) {
1801                     auto it = findPendingEvent(txnId);
1802                     if (it != d->unsyncedEvents.end()) {
1803                         const auto idx = int(it - d->unsyncedEvents.begin());
1804                         emit pendingEventAboutToDiscard(idx);
1805                         // See #286 on why iterator may not be valid here.
1806                         d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx);
1807                         emit pendingEventDiscarded();
1808                     }
1809                 }
1810             });
1811 
1812     return txnId;
1813 }
1814 
postEvent(RoomEvent * event)1815 QString Room::postEvent(RoomEvent* event)
1816 {
1817     return d->sendEvent(RoomEventPtr(event));
1818 }
1819 
postJson(const QString & matrixType,const QJsonObject & eventContent)1820 QString Room::postJson(const QString& matrixType,
1821                        const QJsonObject& eventContent)
1822 {
1823     return d->sendEvent(loadEvent<RoomEvent>(matrixType, eventContent));
1824 }
1825 
setState(const StateEventBase & evt) const1826 SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) const
1827 {
1828     return d->requestSetState(evt);
1829 }
1830 
setName(const QString & newName)1831 void Room::setName(const QString& newName)
1832 {
1833     d->requestSetState<RoomNameEvent>(newName);
1834 }
1835 
setCanonicalAlias(const QString & newAlias)1836 void Room::setCanonicalAlias(const QString& newAlias)
1837 {
1838     d->requestSetState<RoomCanonicalAliasEvent>(newAlias, altAliases());
1839 }
1840 
setLocalAliases(const QStringList & aliases)1841 void Room::setLocalAliases(const QStringList& aliases)
1842 {
1843     d->requestSetState<RoomCanonicalAliasEvent>(canonicalAlias(), aliases);
1844 }
1845 
setTopic(const QString & newTopic)1846 void Room::setTopic(const QString& newTopic)
1847 {
1848     d->requestSetState<RoomTopicEvent>(newTopic);
1849 }
1850 
isEchoEvent(const RoomEventPtr & le,const PendingEventItem & re)1851 bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re)
1852 {
1853     if (le->type() != re->type())
1854         return false;
1855 
1856     if (!re->id().isEmpty())
1857         return le->id() == re->id();
1858     if (!re->transactionId().isEmpty())
1859         return le->transactionId() == re->transactionId();
1860 
1861     // This one is not reliable (there can be two unsynced
1862     // events with the same type, sender and state key) but
1863     // it's the best we have for state events.
1864     if (re->isStateEvent())
1865         return le->stateKey() == re->stateKey();
1866 
1867     // Empty id and no state key, hmm... (shrug)
1868     return le->contentJson() == re->contentJson();
1869 }
1870 
supportsCalls() const1871 bool Room::supportsCalls() const { return joinedCount() == 2; }
1872 
checkVersion()1873 void Room::checkVersion()
1874 {
1875     const auto defaultVersion = connection()->defaultRoomVersion();
1876     const auto stableVersions = connection()->stableRoomVersions();
1877     Q_ASSERT(!defaultVersion.isEmpty());
1878     // This method is only called after the base state has been loaded
1879     // or the server capabilities have been loaded.
1880     emit stabilityUpdated(defaultVersion, stableVersions);
1881     if (!stableVersions.contains(version())) {
1882         qCDebug(STATE) << this << "version is" << version()
1883                        << "which the server doesn't count as stable";
1884         if (canSwitchVersions())
1885             qCDebug(STATE)
1886                 << "The current user has enough privileges to fix it";
1887     }
1888 }
1889 
inviteCall(const QString & callId,const int lifetime,const QString & sdp)1890 void Room::inviteCall(const QString& callId, const int lifetime,
1891                       const QString& sdp)
1892 {
1893     Q_ASSERT(supportsCalls());
1894     d->sendEvent<CallInviteEvent>(callId, lifetime, sdp);
1895 }
1896 
sendCallCandidates(const QString & callId,const QJsonArray & candidates)1897 void Room::sendCallCandidates(const QString& callId,
1898                               const QJsonArray& candidates)
1899 {
1900     Q_ASSERT(supportsCalls());
1901     d->sendEvent<CallCandidatesEvent>(callId, candidates);
1902 }
1903 
answerCall(const QString & callId,const int lifetime,const QString & sdp)1904 void Room::answerCall(const QString& callId, const int lifetime,
1905                       const QString& sdp)
1906 {
1907     Q_ASSERT(supportsCalls());
1908     d->sendEvent<CallAnswerEvent>(callId, lifetime, sdp);
1909 }
1910 
answerCall(const QString & callId,const QString & sdp)1911 void Room::answerCall(const QString& callId, const QString& sdp)
1912 {
1913     Q_ASSERT(supportsCalls());
1914     d->sendEvent<CallAnswerEvent>(callId, sdp);
1915 }
1916 
hangupCall(const QString & callId)1917 void Room::hangupCall(const QString& callId)
1918 {
1919     Q_ASSERT(supportsCalls());
1920     d->sendEvent<CallHangupEvent>(callId);
1921 }
1922 
getPreviousContent(int limit)1923 void Room::getPreviousContent(int limit) { d->getPreviousContent(limit); }
1924 
getPreviousContent(int limit)1925 void Room::Private::getPreviousContent(int limit)
1926 {
1927     if (isJobRunning(eventsHistoryJob))
1928         return;
1929 
1930     eventsHistoryJob =
1931         connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit);
1932     emit q->eventsHistoryJobChanged();
1933     connect(eventsHistoryJob, &BaseJob::success, q, [=] {
1934         prevBatch = eventsHistoryJob->end();
1935         addHistoricalMessageEvents(eventsHistoryJob->chunk());
1936     });
1937     connect(eventsHistoryJob, &QObject::destroyed, q,
1938             &Room::eventsHistoryJobChanged);
1939 }
1940 
inviteToRoom(const QString & memberId)1941 void Room::inviteToRoom(const QString& memberId)
1942 {
1943     connection()->callApi<InviteUserJob>(id(), memberId);
1944 }
1945 
leaveRoom()1946 LeaveRoomJob* Room::leaveRoom()
1947 {
1948     // FIXME, #63: It should be RoomManager, not Connection
1949     return connection()->leaveRoom(this);
1950 }
1951 
setMemberState(const QString & memberId,const RoomMemberEvent & event) const1952 SetRoomStateWithKeyJob* Room::setMemberState(const QString& memberId,
1953                                              const RoomMemberEvent& event) const
1954 {
1955     return d->requestSetState<RoomMemberEvent>(memberId, event.content());
1956 }
1957 
kickMember(const QString & memberId,const QString & reason)1958 void Room::kickMember(const QString& memberId, const QString& reason)
1959 {
1960     connection()->callApi<KickJob>(id(), memberId, reason);
1961 }
1962 
ban(const QString & userId,const QString & reason)1963 void Room::ban(const QString& userId, const QString& reason)
1964 {
1965     connection()->callApi<BanJob>(id(), userId, reason);
1966 }
1967 
unban(const QString & userId)1968 void Room::unban(const QString& userId)
1969 {
1970     connection()->callApi<UnbanJob>(id(), userId);
1971 }
1972 
redactEvent(const QString & eventId,const QString & reason)1973 void Room::redactEvent(const QString& eventId, const QString& reason)
1974 {
1975     connection()->callApi<RedactEventJob>(id(), QUrl::toPercentEncoding(eventId),
1976                                           connection()->generateTxnId(), reason);
1977 }
1978 
uploadFile(const QString & id,const QUrl & localFilename,const QString & overrideContentType)1979 void Room::uploadFile(const QString& id, const QUrl& localFilename,
1980                       const QString& overrideContentType)
1981 {
1982     Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__,
1983                "localFilename should point at a local file");
1984     auto fileName = localFilename.toLocalFile();
1985     auto job = connection()->uploadFile(fileName, overrideContentType);
1986     if (isJobRunning(job)) {
1987         d->fileTransfers[id] = { job, fileName, true };
1988         connect(job, &BaseJob::uploadProgress, this,
1989                 [this, id](qint64 sent, qint64 total) {
1990                     d->fileTransfers[id].update(sent, total);
1991                     emit fileTransferProgress(id, sent, total);
1992                 });
1993         connect(job, &BaseJob::success, this, [this, id, localFilename, job] {
1994             d->fileTransfers[id].status = FileTransferInfo::Completed;
1995             emit fileTransferCompleted(id, localFilename, job->contentUri());
1996         });
1997         connect(job, &BaseJob::failure, this,
1998                 std::bind(&Private::failedTransfer, d, id, job->errorString()));
1999         emit newFileTransfer(id, localFilename);
2000     } else
2001         d->failedTransfer(id);
2002 }
2003 
downloadFile(const QString & eventId,const QUrl & localFilename)2004 void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
2005 {
2006     if (auto ongoingTransfer = d->fileTransfers.constFind(eventId);
2007         ongoingTransfer != d->fileTransfers.cend()
2008         && ongoingTransfer->status == FileTransferInfo::Started) {
2009         qCWarning(MAIN) << "Transfer for" << eventId
2010                         << "is ongoing; download won't start";
2011         return;
2012     }
2013 
2014     Q_ASSERT_X(localFilename.isEmpty() || localFilename.isLocalFile(),
2015                __FUNCTION__, "localFilename should point at a local file");
2016     const auto* event = d->getEventWithFile(eventId);
2017     if (!event) {
2018         qCCritical(MAIN)
2019             << eventId << "is not in the local timeline or has no file content";
2020         Q_ASSERT(false);
2021         return;
2022     }
2023     const auto* const fileInfo = event->content()->fileInfo();
2024     if (!fileInfo->isValid()) {
2025         qCWarning(MAIN) << "Event" << eventId
2026                         << "has an empty or malformed mxc URL; won't download";
2027         return;
2028     }
2029     const auto fileUrl = fileInfo->url;
2030     auto filePath = localFilename.toLocalFile();
2031     if (filePath.isEmpty()) { // Setup default file path
2032         filePath =
2033             fileInfo->url.path().mid(1) % '_' % d->fileNameToDownload(event);
2034 
2035         if (filePath.size() > 200) // If too long, elide in the middle
2036             filePath.replace(128, filePath.size() - 192, "---");
2037 
2038         filePath = QDir::tempPath() % '/' % filePath;
2039         qDebug(MAIN) << "File path:" << filePath;
2040     }
2041     auto job = connection()->downloadFile(fileUrl, filePath);
2042     if (isJobRunning(job)) {
2043         // If there was a previous transfer (completed or failed), overwrite it.
2044         d->fileTransfers[eventId] = { job, job->targetFileName() };
2045         connect(job, &BaseJob::downloadProgress, this,
2046                 [this, eventId](qint64 received, qint64 total) {
2047                     d->fileTransfers[eventId].update(received, total);
2048                     emit fileTransferProgress(eventId, received, total);
2049                 });
2050         connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] {
2051             d->fileTransfers[eventId].status = FileTransferInfo::Completed;
2052             emit fileTransferCompleted(
2053                 eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName()));
2054         });
2055         connect(job, &BaseJob::failure, this,
2056                 std::bind(&Private::failedTransfer, d, eventId,
2057                           job->errorString()));
2058     } else
2059         d->failedTransfer(eventId);
2060 }
2061 
cancelFileTransfer(const QString & id)2062 void Room::cancelFileTransfer(const QString& id)
2063 {
2064     const auto it = d->fileTransfers.constFind(id);
2065     if (it == d->fileTransfers.cend()) {
2066         qCWarning(MAIN) << "No information on file transfer" << id << "in room"
2067                         << d->id;
2068         return;
2069     }
2070     if (isJobRunning(it->job))
2071         it->job->abandon();
2072     d->fileTransfers.remove(id);
2073     emit fileTransferCancelled(id);
2074 }
2075 
dropDuplicateEvents(RoomEvents & events) const2076 void Room::Private::dropDuplicateEvents(RoomEvents& events) const
2077 {
2078     if (events.empty())
2079         return;
2080 
2081     // Multiple-remove (by different criteria), single-erase
2082     // 1. Check for duplicates against the timeline.
2083     auto dupsBegin =
2084         remove_if(events.begin(), events.end(), [&](const RoomEventPtr& e) {
2085             return eventsIndex.contains(e->id());
2086         });
2087 
2088     // 2. Check for duplicates within the batch if there are still events.
2089     for (auto eIt = events.begin(); distance(eIt, dupsBegin) > 1; ++eIt)
2090         dupsBegin = remove_if(eIt + 1, dupsBegin, [&](const RoomEventPtr& e) {
2091             return e->id() == (*eIt)->id();
2092         });
2093     if (dupsBegin == events.end())
2094         return;
2095 
2096     qCDebug(EVENTS) << "Dropping" << distance(dupsBegin, events.end())
2097                     << "duplicate event(s)";
2098     events.erase(dupsBegin, events.end());
2099 }
2100 
2101 /** Make a redacted event
2102  *
2103  * This applies the redaction procedure as defined by the CS API specification
2104  * to the event's JSON and returns the resulting new event. It is
2105  * the responsibility of the caller to dispose of the original event after that.
2106  */
makeRedacted(const RoomEvent & target,const RedactionEvent & redaction)2107 RoomEventPtr makeRedacted(const RoomEvent& target,
2108                           const RedactionEvent& redaction)
2109 {
2110     auto originalJson = target.originalJsonObject();
2111     // clang-format off
2112     static const QStringList keepKeys { EventIdKey, TypeKey,
2113         QStringLiteral("room_id"), QStringLiteral("sender"), StateKeyKey,
2114         QStringLiteral("hashes"), QStringLiteral("signatures"),
2115         QStringLiteral("depth"), QStringLiteral("prev_events"),
2116         QStringLiteral("prev_state"), QStringLiteral("auth_events"),
2117         QStringLiteral("origin"), QStringLiteral("origin_server_ts"),
2118         QStringLiteral("membership") };
2119     // clang-format on
2120 
2121     std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap {
2122         { RoomMemberEvent::typeId(), { QStringLiteral("membership") } },
2123         { RoomCreateEvent::typeId(), { QStringLiteral("creator") } },
2124         { RoomPowerLevelsEvent::typeId(),
2125           { QStringLiteral("ban"), QStringLiteral("events"),
2126             QStringLiteral("events_default"), QStringLiteral("kick"),
2127             QStringLiteral("redact"), QStringLiteral("state_default"),
2128             QStringLiteral("users"), QStringLiteral("users_default") } },
2129         { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } }
2130         //        , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } }
2131         //        , { RoomHistoryVisibility::typeId(),
2132         //                { QStringLiteral("history_visibility") } }
2133     };
2134     for (auto it = originalJson.begin(); it != originalJson.end();) {
2135         if (!keepKeys.contains(it.key()))
2136             it = originalJson.erase(it); // TODO: shred the value
2137         else
2138             ++it;
2139     }
2140     auto keepContentKeys =
2141         find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(),
2142                 [&target](const auto& t) { return target.type() == t.first; });
2143     if (keepContentKeys == keepContentKeysMap.end()) {
2144         originalJson.remove(ContentKeyL);
2145         originalJson.remove(PrevContentKeyL);
2146     } else {
2147         auto content = originalJson.take(ContentKeyL).toObject();
2148         for (auto it = content.begin(); it != content.end();) {
2149             if (!keepContentKeys->second.contains(it.key()))
2150                 it = content.erase(it);
2151             else
2152                 ++it;
2153         }
2154         originalJson.insert(ContentKey, content);
2155     }
2156     auto unsignedData = originalJson.take(UnsignedKeyL).toObject();
2157     unsignedData[RedactedCauseKeyL] = redaction.originalJsonObject();
2158     originalJson.insert(QStringLiteral("unsigned"), unsignedData);
2159 
2160     return loadEvent<RoomEvent>(originalJson);
2161 }
2162 
processRedaction(const RedactionEvent & redaction)2163 bool Room::Private::processRedaction(const RedactionEvent& redaction)
2164 {
2165     // Can't use findInTimeline because it returns a const iterator, and
2166     // we need to change the underlying TimelineItem.
2167     const auto pIdx = eventsIndex.constFind(redaction.redactedEvent());
2168     if (pIdx == eventsIndex.cend())
2169         return false;
2170 
2171     Q_ASSERT(q->isValidIndex(*pIdx));
2172 
2173     auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())];
2174     if (ti->isRedacted() && ti->redactedBecause()->id() == redaction.id()) {
2175         qCDebug(EVENTS) << "Redaction" << redaction.id() << "of event"
2176                         << ti->id() << "already done, skipping";
2177         return true;
2178     }
2179 
2180     // Make a new event from the redacted JSON and put it in the timeline
2181     // instead of the redacted one. oldEvent will be deleted on return.
2182     auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction));
2183     qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id();
2184     if (oldEvent->isStateEvent()) {
2185         const StateEventKey evtKey { oldEvent->matrixType(),
2186                                      oldEvent->stateKey() };
2187         Q_ASSERT(currentState.contains(evtKey));
2188         if (currentState.value(evtKey) == oldEvent.get()) {
2189             Q_ASSERT(ti.index() >= 0); // Historical states can't be in
2190                                        // currentState
2191             qCDebug(STATE).nospace()
2192                 << "Redacting state " << oldEvent->matrixType() << "/"
2193                 << oldEvent->stateKey();
2194             // Retarget the current state to the newly made event.
2195             if (q->processStateEvent(*ti))
2196                 emit q->namesChanged(q);
2197             updateDisplayname();
2198         }
2199     }
2200     if (const auto* reaction = eventCast<ReactionEvent>(oldEvent)) {
2201         const auto& targetEvtId = reaction->relation().eventId;
2202         const auto lookupKey =
2203             qMakePair(targetEvtId, EventRelation::Annotation());
2204         if (relations.contains(lookupKey)) {
2205             relations[lookupKey].removeOne(reaction);
2206             emit q->updatedEvent(targetEvtId);
2207         }
2208     }
2209     q->onRedaction(*oldEvent, *ti);
2210     emit q->replacedEvent(ti.event(), rawPtr(oldEvent));
2211     return true;
2212 }
2213 
2214 /** Make a replaced event
2215  *
2216  * Takes \p target and returns a copy of it with content taken from
2217  * \p replacement. Disposal of the original event after that is on the caller.
2218  */
makeReplaced(const RoomEvent & target,const RoomMessageEvent & replacement)2219 RoomEventPtr makeReplaced(const RoomEvent& target,
2220                           const RoomMessageEvent& replacement)
2221 {
2222     auto originalJson = target.originalJsonObject();
2223     originalJson[ContentKeyL] = replacement.contentJson().value("m.new_content"_ls);
2224 
2225     auto unsignedData = originalJson.take(UnsignedKeyL).toObject();
2226     auto relations = unsignedData.take("m.relations"_ls).toObject();
2227     relations["m.replace"_ls] = replacement.id();
2228     unsignedData.insert(QStringLiteral("m.relations"), relations);
2229     originalJson.insert(UnsignedKey, unsignedData);
2230 
2231     return loadEvent<RoomEvent>(originalJson);
2232 }
2233 
processReplacement(const RoomMessageEvent & newEvent)2234 bool Room::Private::processReplacement(const RoomMessageEvent& newEvent)
2235 {
2236     // Can't use findInTimeline because it returns a const iterator, and
2237     // we need to change the underlying TimelineItem.
2238     const auto pIdx = eventsIndex.constFind(newEvent.replacedEvent());
2239     if (pIdx == eventsIndex.cend())
2240         return false;
2241 
2242     Q_ASSERT(q->isValidIndex(*pIdx));
2243 
2244     auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())];
2245     if (ti->replacedBy() == newEvent.id()) {
2246         qCDebug(STATE) << "Event" << ti->id() << "is already replaced with"
2247                        << newEvent.id();
2248         return true;
2249     }
2250 
2251     // Make a new event from the redacted JSON and put it in the timeline
2252     // instead of the redacted one. oldEvent will be deleted on return.
2253     auto oldEvent = ti.replaceEvent(makeReplaced(*ti, newEvent));
2254     qCDebug(STATE) << "Replaced" << oldEvent->id() << "with" << newEvent.id();
2255     emit q->replacedEvent(ti.event(), rawPtr(oldEvent));
2256     return true;
2257 }
2258 
connection() const2259 Connection* Room::connection() const
2260 {
2261     Q_ASSERT(d->connection);
2262     return d->connection;
2263 }
2264 
localUser() const2265 User* Room::localUser() const { return connection()->user(); }
2266 
2267 /// Whether the event is a redaction or a replacement
isEditing(const RoomEventPtr & ep)2268 inline bool isEditing(const RoomEventPtr& ep)
2269 {
2270     Q_ASSERT(ep);
2271     if (is<RedactionEvent>(*ep))
2272         return true;
2273     if (auto* msgEvent = eventCast<RoomMessageEvent>(ep))
2274         return !msgEvent->replacedEvent().isEmpty();
2275 
2276     return false;
2277 }
2278 
addNewMessageEvents(RoomEvents && events)2279 Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
2280 {
2281     dropDuplicateEvents(events);
2282     if (events.empty())
2283         return Change::NoChange;
2284 
2285     QElapsedTimer et;
2286     et.start();
2287     {
2288         // Pre-process redactions and edits so that events that get
2289         // redacted/replaced in the same batch landed in the timeline already
2290         // treated.
2291         // NB: We have to store redacting/replacing events to the timeline too -
2292         // see #220.
2293         auto it = std::find_if(events.begin(), events.end(), isEditing);
2294         for (const auto& eptr : RoomEventsRange(it, events.end())) {
2295             if (auto* r = eventCast<RedactionEvent>(eptr)) {
2296                 // Try to find the target in the timeline, then in the batch.
2297                 if (processRedaction(*r))
2298                     continue;
2299                 if (auto targetIt = std::find_if(events.begin(), events.end(),
2300                         [id = r->redactedEvent()](const RoomEventPtr& ep) {
2301                             return ep->id() == id;
2302                         }); targetIt != events.end())
2303                     *targetIt = makeRedacted(**targetIt, *r);
2304                 else
2305                     qCDebug(STATE)
2306                         << "Redaction" << r->id() << "ignored: target event"
2307                         << r->redactedEvent() << "is not found";
2308                 // If the target event comes later, it comes already redacted.
2309             }
2310             if (auto* msg = eventCast<RoomMessageEvent>(eptr);
2311                     msg && !msg->replacedEvent().isEmpty()) {
2312                 if (processReplacement(*msg))
2313                     continue;
2314                 auto targetIt = std::find_if(events.begin(), it,
2315                         [id = msg->replacedEvent()](const RoomEventPtr& ep) {
2316                             return ep->id() == id;
2317                         });
2318                 if (targetIt != it)
2319                     *targetIt = makeReplaced(**targetIt, *msg);
2320                 else // FIXME: hide the replacing event when target arrives later
2321                     qCDebug(EVENTS)
2322                         << "Replacing event" << msg->id()
2323                         << "ignored: target event" << msg->replacedEvent()
2324                         << "is not found";
2325                 // Same as with redactions above, the replaced event coming
2326                 // later will come already with the new content.
2327             }
2328         }
2329     }
2330 
2331     // State changes arrive as a part of timeline; the current room state gets
2332     // updated before merging events to the timeline because that's what
2333     // clients historically expect. This may eventually change though if we
2334     // postulate that the current state is only current between syncs but not
2335     // within a sync.
2336     Changes roomChanges = Change::NoChange;
2337     for (const auto& eptr : events)
2338         roomChanges |= q->processStateEvent(*eptr);
2339 
2340     auto timelineSize = timeline.size();
2341     size_t totalInserted = 0;
2342     for (auto it = events.begin(); it != events.end();) {
2343         auto nextPendingPair =
2344                     findFirstOf(it, events.end(), unsyncedEvents.begin(),
2345                                 unsyncedEvents.end(), isEchoEvent);
2346                 const auto& remoteEcho = nextPendingPair.first;
2347                 const auto& localEcho = nextPendingPair.second;
2348 
2349         if (it != remoteEcho) {
2350             RoomEventsRange eventsSpan { it, remoteEcho };
2351             emit q->aboutToAddNewMessages(eventsSpan);
2352             auto insertedSize = moveEventsToTimeline(eventsSpan, Newer);
2353             totalInserted += insertedSize;
2354             auto firstInserted = syncEdge() - insertedSize;
2355             q->onAddNewTimelineEvents(firstInserted);
2356             emit q->addedMessages(firstInserted->index(),
2357                                   timeline.back().index());
2358         }
2359         if (remoteEcho == events.end())
2360             break;
2361 
2362         it = remoteEcho + 1;
2363         auto* nextPendingEvt = remoteEcho->get();
2364         const auto pendingEvtIdx = int(localEcho - unsyncedEvents.begin());
2365         if (localEcho->deliveryStatus() != EventStatus::ReachedServer) {
2366             localEcho->setReachedServer(nextPendingEvt->id());
2367             emit q->pendingEventChanged(pendingEvtIdx);
2368         }
2369         emit q->pendingEventAboutToMerge(nextPendingEvt, pendingEvtIdx);
2370         qCDebug(MESSAGES) << "Merging pending event from transaction"
2371                          << nextPendingEvt->transactionId() << "into"
2372                          << nextPendingEvt->id();
2373         auto transfer = fileTransfers.take(nextPendingEvt->transactionId());
2374         if (transfer.status != FileTransferInfo::None)
2375             fileTransfers.insert(nextPendingEvt->id(), transfer);
2376         // After emitting pendingEventAboutToMerge() above we cannot rely
2377         // on the previously obtained localEcho staying valid
2378         // because a signal handler may send another message, thereby altering
2379         // unsyncedEvents (see #286). Fortunately, unsyncedEvents only grows at
2380         // its back so we can rely on the index staying valid at least.
2381         unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx);
2382         if (auto insertedSize = moveEventsToTimeline({ remoteEcho, it }, Newer)) {
2383             totalInserted += insertedSize;
2384             q->onAddNewTimelineEvents(syncEdge() - insertedSize);
2385         }
2386         emit q->pendingEventMerged();
2387     }
2388     // Events merged and transferred from `events` to `timeline` now.
2389     const auto from = syncEdge() - totalInserted;
2390 
2391     if (q->supportsCalls())
2392         for (auto it = from; it != syncEdge(); ++it)
2393             if (const auto* evt = it->viewAs<CallEventBase>())
2394                 emit q->callEvent(q, evt);
2395 
2396     if (totalInserted > 0) {
2397         for (auto it = from; it != syncEdge(); ++it) {
2398             if (const auto* reaction = it->viewAs<ReactionEvent>()) {
2399                 const auto& relation = reaction->relation();
2400                 relations[{ relation.eventId, relation.type }] << reaction;
2401                 emit q->updatedEvent(relation.eventId);
2402             }
2403         }
2404 
2405         qCDebug(MESSAGES) << "Room" << q->objectName() << "received"
2406                        << totalInserted << "new events; the last event is now"
2407                        << timeline.back();
2408 
2409         // The first event in the just-added batch (referred to by `from`)
2410         // defines whose read receipt can possibly be promoted any further over
2411         // the same author's events newly arrived. Others will need explicit
2412         // read receipts from the server - or, for the local user, calling
2413         // setLastDisplayedEventId() - to promote their read receipts over
2414         // the new message events.
2415         if (auto* const firstWriter = q->user((*from)->senderId())) {
2416             setLastReadReceipt(firstWriter, rev_iter_t(from + 1));
2417             if (firstWriter == q->localUser() && q->readMarker().base() == from) {
2418                 // If the local user's message(s) is/are first in the batch
2419                 // and the fully read marker was right before it, promote
2420                 // the fully read marker to the same event as the read receipt.
2421                 roomChanges |=
2422                     setFullyReadMarker(lastReadEventIds.value(firstWriter));
2423             }
2424         }
2425         roomChanges |= updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
2426     }
2427 
2428     Q_ASSERT(timeline.size() == timelineSize + totalInserted);
2429     if (totalInserted > 9 || et.nsecsElapsed() >= profilerMinNsecs())
2430         qCDebug(PROFILER) << "Added" << totalInserted << "new event(s) to"
2431                           << q->objectName() << "in" << et;
2432     return roomChanges;
2433 }
2434 
addHistoricalMessageEvents(RoomEvents && events)2435 void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
2436 {
2437     QElapsedTimer et;
2438     et.start();
2439     const auto timelineSize = timeline.size();
2440 
2441     dropDuplicateEvents(events);
2442     if (events.empty())
2443         return;
2444 
2445     // In case of lazy-loading new members may be loaded with historical
2446     // messages. Also, the cache doesn't store events with empty content;
2447     // so when such events show up in the timeline they should be properly
2448     // incorporated.
2449     for (const auto& eptr : events) {
2450         const auto& e = *eptr;
2451         if (e.isStateEvent()
2452             && !currentState.contains({ e.matrixType(), e.stateKey() })) {
2453             q->processStateEvent(e);
2454         }
2455     }
2456 
2457     emit q->aboutToAddHistoricalMessages(events);
2458     const auto insertedSize = moveEventsToTimeline(events, Older);
2459     const auto from = historyEdge() - insertedSize;
2460 
2461     qCDebug(STATE) << "Room" << displayname << "received" << insertedSize
2462                    << "past events; the oldest event is now" << timeline.front();
2463     q->onAddHistoricalTimelineEvents(from);
2464     emit q->addedMessages(timeline.front().index(), from->index());
2465 
2466     for (auto it = from; it != historyEdge(); ++it) {
2467         if (const auto* reaction = it->viewAs<ReactionEvent>()) {
2468             const auto& relation = reaction->relation();
2469             relations[{ relation.eventId, relation.type }] << reaction;
2470             emit q->updatedEvent(relation.eventId);
2471         }
2472     }
2473     if (updateUnreadCount(from, historyEdge()) != NoChange)
2474         connection->saveRoomState(q);
2475 
2476     // When there are no unread messages and the read marker is within the
2477     // known timeline, unreadMessages == -1
2478     // (see https://github.com/quotient-im/libQuotient/wiki/unread_count).
2479     Q_ASSERT(unreadMessages != 0 || q->readMarker() == historyEdge());
2480 
2481     Q_ASSERT(timeline.size() == timelineSize + insertedSize);
2482     if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs())
2483         qCDebug(PROFILER) << "Added" << insertedSize << "historical event(s) to"
2484                           << q->objectName() << "in" << et;
2485 }
2486 
processStateEvent(const RoomEvent & e)2487 Room::Changes Room::processStateEvent(const RoomEvent& e)
2488 {
2489     if (!e.isStateEvent())
2490         return Change::NoChange;
2491 
2492     auto* const sender = user(e.senderId());
2493     if (!sender) {
2494         qCWarning(MAIN) << "State event" << e.id()
2495                         << "is invalid and won't be processed";
2496         return Change::NoChange;
2497     }
2498 
2499     // Find a value (create an empty one if necessary) and get a reference
2500     // to it. Can't use getCurrentState<>() because it (creates and) returns
2501     // a stub if a value is not found, and what's needed here is a "real" event
2502     // or nullptr.
2503     auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }];
2504     // Prepare for the state change
2505     const auto oldRme = static_cast<const RoomMemberEvent*>(curStateEvent);
2506     visit(e, [this, &oldRme](const RoomMemberEvent& rme) {
2507         auto* const u = user(rme.userId());
2508         if (!u) { // Invalid user id?
2509             qCWarning(MAIN)
2510                 << "Could not get a user object for" << rme.userId();
2511             return;
2512         }
2513         // TODO: remove along with User::processEvent() in 0.7
2514         const auto prevMembership = oldRme ? oldRme->membership()
2515                                            : MembershipType::Leave;
2516         u->processEvent(rme, this, oldRme == nullptr);
2517 
2518         switch (prevMembership) {
2519         case MembershipType::Invite:
2520             if (rme.membership() != prevMembership) {
2521                 d->usersInvited.removeOne(u);
2522                 Q_ASSERT(!d->usersInvited.contains(u));
2523             }
2524             break;
2525         case MembershipType::Join:
2526             switch (rme.membership()) {
2527             case MembershipType::Join: // rename/avatar change or no-op
2528                 if (rme.displayName() != oldRme->displayName()) {
2529                     emit memberAboutToRename(u, rme.displayName());
2530                     d->removeMemberFromMap(u);
2531                 }
2532                 break;
2533             case MembershipType::Invite:
2534                 qCWarning(MAIN) << "Membership change from Join to Invite:"
2535                                 << rme;
2536                 [[fallthrough]];
2537             default: // whatever the new membership, it's no more Join
2538                 d->removeMemberFromMap(u);
2539                 emit userRemoved(u);
2540             }
2541             break;
2542         default:
2543             if (rme.membership() == MembershipType::Invite
2544                 || rme.membership() == MembershipType::Join) {
2545                 d->membersLeft.removeOne(u);
2546                 Q_ASSERT(!d->membersLeft.contains(u));
2547             }
2548         }
2549     });
2550 
2551     // Change the state
2552     const auto* const oldStateEvent =
2553         std::exchange(curStateEvent, static_cast<const StateEventBase*>(&e));
2554     Q_ASSERT(!oldStateEvent
2555              || (oldStateEvent->matrixType() == e.matrixType()
2556                  && oldStateEvent->stateKey() == e.stateKey()));
2557     if (!is<RoomMemberEvent>(e)) // Room member events are too numerous
2558         qCDebug(STATE) << "Updated room state:" << e;
2559 
2560     // Update internal structures as per the change and work out the return value
2561 
2562     // clang-format off
2563     return visit(e
2564         , [] (const RoomNameEvent&) {
2565             return NameChange;
2566         }
2567         , [] (const RoomAliasesEvent&) {
2568             return NoChange; // This event has been removed by MSC2432
2569         }
2570         , [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) {
2571             // clang-format on
2572             setObjectName(cae.alias().isEmpty() ? d->id : cae.alias());
2573             const auto* oldCae =
2574                     static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent);
2575             QStringList previousAltAliases {};
2576             if (oldCae) {
2577                 previousAltAliases = oldCae->altAliases();
2578                 if (!oldCae->alias().isEmpty())
2579                     previousAltAliases.push_back(oldCae->alias());
2580             }
2581 
2582             auto newAliases = cae.altAliases();
2583             if (!cae.alias().isEmpty())
2584                 newAliases.push_front(cae.alias());
2585 
2586             connection()->updateRoomAliases(id(), previousAltAliases, newAliases);
2587             return AliasesChange;
2588             // clang-format off
2589         }
2590         , [] (const RoomTopicEvent&) {
2591             return TopicChange;
2592         }
2593         , [this] (const RoomAvatarEvent& evt) {
2594             if (d->avatar.updateUrl(evt.url()))
2595                 emit avatarChanged();
2596             return AvatarChange;
2597         }
2598         , [this,oldRme,sender] (const RoomMemberEvent& evt) {
2599             // clang-format on
2600             auto* u = user(evt.userId());
2601             if (!u)
2602                 return NoChange; // Already warned earlier
2603             // TODO: remove in 0.7
2604             u->processEvent(evt, this, oldRme == nullptr);
2605 
2606             const auto prevMembership = oldRme ? oldRme->membership()
2607                                                : MembershipType::Leave;
2608             switch (evt.membership()) {
2609             case MembershipType::Join:
2610                 if (prevMembership != MembershipType::Join) {
2611                     d->insertMemberIntoMap(u);
2612                     emit userAdded(u);
2613                 } else if (oldRme->displayName() != evt.displayName()) {
2614                     d->insertMemberIntoMap(u);
2615                     emit memberRenamed(u);
2616                 }
2617                 break;
2618             case MembershipType::Invite:
2619                 if (!d->usersInvited.contains(u))
2620                     d->usersInvited.push_back(u);
2621                 if (u == localUser() && evt.isDirect())
2622                     connection()->addToDirectChats(this, sender);
2623                 break;
2624             case MembershipType::Knock:
2625             case MembershipType::Ban:
2626             case MembershipType::Leave:
2627                 if (!d->membersLeft.contains(u))
2628                     d->membersLeft.append(u);
2629             }
2630             return MembersChange;
2631             // clang-format off
2632         }
2633         , [this, oldEncEvt = static_cast<const EncryptionEvent*>(oldStateEvent)](
2634             const EncryptionEvent& ee) {
2635             // clang-format on
2636             if (ee.algorithm().isEmpty()) {
2637                 qWarning(STATE)
2638                     << "The encryption event for room" << objectName()
2639                     << "doesn't have 'algorithm' specified - ignoring";
2640                 return NoChange;
2641             }
2642             if (oldEncEvt
2643                 && oldEncEvt->encryption() != EncryptionEventContent::Undefined) {
2644                 qCWarning(STATE) << "The room is already encrypted but a new"
2645                                     " room encryption event arrived - ignoring";
2646                 return NoChange;
2647             }
2648             // As encryption can only be switched on once, emit the signal here
2649             // instead of aggregating and emitting in updateData()
2650             emit encryption();
2651             return OtherChange;
2652             // clang-format off
2653         }
2654         , [this] (const RoomTombstoneEvent& evt) {
2655             const auto successorId = evt.successorRoomId();
2656             if (auto* successor = connection()->room(successorId))
2657                 emit upgraded(evt.serverMessage(), successor);
2658             else
2659                 connectUntil(connection(), &Connection::loadedRoomState, this,
2660                     [this,successorId,serverMsg=evt.serverMessage()]
2661                     (Room* newRoom) {
2662                         if (newRoom->id() != successorId)
2663                             return false;
2664                         emit upgraded(serverMsg, newRoom);
2665                         return true;
2666                     });
2667 
2668             return OtherChange;
2669         }
2670     );
2671     // clang-format on
2672 }
2673 
processEphemeralEvent(EventPtr && event)2674 Room::Changes Room::processEphemeralEvent(EventPtr&& event)
2675 {
2676     Changes changes = NoChange;
2677     QElapsedTimer et;
2678     et.start();
2679     if (auto* evt = eventCast<TypingEvent>(event)) {
2680         d->usersTyping.clear();
2681         for (const QString& userId : qAsConst(evt->users())) {
2682             auto* const u = user(userId);
2683             if (memberJoinState(u) == JoinState::Join)
2684                 d->usersTyping.append(u);
2685         }
2686         if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs())
2687             qCDebug(PROFILER)
2688                 << "Processing typing events from" << evt->users().size()
2689                 << "user(s) in" << objectName() << "took" << et;
2690         emit typingChanged();
2691     }
2692     if (auto* evt = eventCast<ReceiptEvent>(event)) {
2693         int totalReceipts = 0;
2694         for (const auto& p : qAsConst(evt->eventsWithReceipts())) {
2695             totalReceipts += p.receipts.size();
2696             {
2697                 if (p.receipts.size() == 1)
2698                     qCDebug(EPHEMERAL)
2699                         << objectName() << "received a read receipt for"
2700                         << p.evtId << "from" << p.receipts[0].userId;
2701                 else
2702                     qCDebug(EPHEMERAL)
2703                         << objectName() << "received read receipts for"
2704                         << p.evtId << "from" << p.receipts.size() << "users";
2705             }
2706             const auto newMarker = findInTimeline(p.evtId);
2707             if (newMarker == historyEdge())
2708                 qCDebug(EPHEMERAL) << "Event of the read receipt(s) is not "
2709                                       "found; saving them anyway";
2710             for (const Receipt& r : p.receipts)
2711                 if (auto* const u = user(r.userId);
2712                     memberJoinState(u) == JoinState::Join) {
2713                     // If the event is not found (most likely, because it's
2714                     // too old and hasn't been fetched from the server yet)
2715                     // but there is a previous marker for a user, keep
2716                     // the previous marker because read receipts are not
2717                     // supposed to move backwards. Otherwise, blindly
2718                     // store the event id for this user and update the read
2719                     // marker when/if the event is fetched later on.
2720                     d->setLastReadReceipt(u, newMarker, p.evtId);
2721                 }
2722         }
2723         if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10
2724             || et.nsecsElapsed() >= profilerMinNsecs())
2725             qCDebug(PROFILER) << "Processing" << totalReceipts << "receipt(s) on"
2726                               << evt->eventsWithReceipts().size()
2727                               << "event(s) in" << objectName() << "took" << et;
2728     }
2729     return changes;
2730 }
2731 
processAccountDataEvent(EventPtr && event)2732 Room::Changes Room::processAccountDataEvent(EventPtr&& event)
2733 {
2734     Changes changes = NoChange;
2735     if (auto* evt = eventCast<TagEvent>(event)) {
2736         d->setTags(evt->tags());
2737         changes |= Change::TagsChange;
2738     }
2739 
2740     if (auto* evt = eventCast<const ReadMarkerEvent>(event))
2741         changes |= d->setFullyReadMarker(evt->event_id());
2742 
2743     // For all account data events
2744     auto& currentData = d->accountData[event->matrixType()];
2745     // A polymorphic event-specific comparison might be a bit more
2746     // efficient; maaybe do it another day
2747     if (!currentData || currentData->contentJson() != event->contentJson()) {
2748         emit accountDataAboutToChange(event->matrixType());
2749         currentData = move(event);
2750         qCDebug(STATE) << "Updated account data of type"
2751                        << currentData->matrixType();
2752         emit accountDataChanged(currentData->matrixType());
2753         changes |= Change::AccountDataChange;
2754     }
2755     return changes;
2756 }
2757 
2758 template <typename ContT>
2759 Room::Private::users_shortlist_t
buildShortlist(const ContT & users) const2760 Room::Private::buildShortlist(const ContT& users) const
2761 {
2762     // To calculate room display name the spec requires to sort users
2763     // lexicographically by state_key (user id) and use disambiguated
2764     // display names of two topmost users excluding the current one to render
2765     // the name of the room. The below code selects 3 topmost users,
2766     // slightly extending the spec.
2767     users_shortlist_t shortlist {}; // Prefill with nullptrs
2768     std::partial_sort_copy(
2769         users.begin(), users.end(), shortlist.begin(), shortlist.end(),
2770         [this](const User* u1, const User* u2) {
2771             // localUser(), if it's in the list, is sorted
2772             // below all others
2773             return isLocalUser(u2) || (!isLocalUser(u1) && u1->id() < u2->id());
2774         });
2775     return shortlist;
2776 }
2777 
2778 Room::Private::users_shortlist_t
buildShortlist(const QStringList & userIds) const2779 Room::Private::buildShortlist(const QStringList& userIds) const
2780 {
2781     QList<User*> users;
2782     users.reserve(userIds.size());
2783     for (const auto& h : userIds)
2784         users.push_back(q->user(h));
2785     return buildShortlist(users);
2786 }
2787 
calculateDisplayname() const2788 QString Room::Private::calculateDisplayname() const
2789 {
2790     // CS spec, section 13.2.2.5 Calculating the display name for a room
2791     // Numbers below refer to respective parts in the spec.
2792 
2793     // 1. Name (from m.room.name)
2794     auto dispName = q->name();
2795     if (!dispName.isEmpty()) {
2796         return dispName;
2797     }
2798 
2799     // 2. Canonical alias
2800     dispName = q->canonicalAlias();
2801     if (!dispName.isEmpty())
2802         return dispName;
2803 
2804     // 3. m.room.aliases - only local aliases, subject for further removal
2805     const auto aliases = q->aliases();
2806     if (!aliases.isEmpty())
2807         return aliases.front();
2808 
2809     // 4. m.heroes and m.room.member
2810     // From here on, we use a more general algorithm than the spec describes
2811     // in order to provide back-compatibility with pre-MSC688 servers.
2812 
2813     // Supplementary code: build the shortlist of users whose names
2814     // will be used to construct the room name. Takes into account MSC688's
2815     // "heroes" if available.
2816     const bool localUserIsIn = joinState == JoinState::Join;
2817     const bool emptyRoom =
2818         membersMap.isEmpty()
2819         || (membersMap.size() == 1 && isLocalUser(*membersMap.cbegin()));
2820     const bool nonEmptySummary = summary.heroes && !summary.heroes->empty();
2821     auto shortlist = nonEmptySummary ? buildShortlist(*summary.heroes)
2822                                      : !emptyRoom ? buildShortlist(membersMap)
2823                                                   : users_shortlist_t {};
2824 
2825     // When the heroes list is there, we can rely on it. If the heroes list is
2826     // missing, the below code gathers invited, or, if there are no invitees,
2827     // left members.
2828     if (!shortlist.front() && localUserIsIn)
2829         shortlist = buildShortlist(usersInvited);
2830 
2831     if (!shortlist.front())
2832         shortlist = buildShortlist(membersLeft);
2833 
2834     QStringList names;
2835     for (auto u : shortlist) {
2836         if (u == nullptr || isLocalUser(u))
2837             break;
2838         // Only disambiguate if the room is not empty
2839         names.push_back(u->displayname(emptyRoom ? nullptr : q));
2840     }
2841 
2842     const auto usersCountExceptLocal =
2843         !emptyRoom
2844             ? q->joinedCount() - int(joinState == JoinState::Join)
2845             : !usersInvited.empty()
2846                   ? usersInvited.count()
2847                   : membersLeft.size() - int(joinState == JoinState::Leave);
2848     if (usersCountExceptLocal > int(shortlist.size()))
2849         names << tr(
2850             "%Ln other(s)",
2851             "Used to make a room name from user names: A, B and _N others_",
2852             usersCountExceptLocal - int(shortlist.size()));
2853     const auto namesList = QLocale().createSeparatedList(names);
2854 
2855     // Room members
2856     if (!emptyRoom)
2857         return namesList;
2858 
2859     // (Spec extension) Invited users
2860     if (!usersInvited.empty())
2861         return tr("Empty room (invited: %1)").arg(namesList);
2862 
2863     // Users that previously left the room
2864     if (!membersLeft.isEmpty())
2865         return tr("Empty room (was: %1)").arg(namesList);
2866 
2867     // Fail miserably
2868     return tr("Empty room (%1)").arg(id);
2869 }
2870 
updateDisplayname()2871 void Room::Private::updateDisplayname()
2872 {
2873     auto swappedName = calculateDisplayname();
2874     if (swappedName != displayname) {
2875         emit q->displaynameAboutToChange(q);
2876         swap(displayname, swappedName);
2877         qCDebug(MAIN) << q->objectName() << "has changed display name from"
2878                      << swappedName << "to" << displayname;
2879         emit q->displaynameChanged(q, swappedName);
2880     }
2881 }
2882 
toJson() const2883 QJsonObject Room::Private::toJson() const
2884 {
2885     QElapsedTimer et;
2886     et.start();
2887     QJsonObject result;
2888     addParam<IfNotEmpty>(result, QStringLiteral("summary"), summary);
2889     {
2890         QJsonArray stateEvents;
2891 
2892         for (const auto* evt : currentState) {
2893             Q_ASSERT(evt->isStateEvent());
2894             if ((evt->isRedacted() && !is<RoomMemberEvent>(*evt))
2895                 || evt->contentJson().isEmpty())
2896                 continue;
2897 
2898             auto json = evt->fullJson();
2899             auto unsignedJson = evt->unsignedJson();
2900             unsignedJson.remove(QStringLiteral("prev_content"));
2901             json[UnsignedKeyL] = unsignedJson;
2902             stateEvents.append(json);
2903         }
2904 
2905         const auto stateObjName = joinState == JoinState::Invite
2906                                       ? QStringLiteral("invite_state")
2907                                       : QStringLiteral("state");
2908         result.insert(stateObjName,
2909                       QJsonObject { { QStringLiteral("events"), stateEvents } });
2910     }
2911 
2912     if (!accountData.empty()) {
2913         QJsonArray accountDataEvents;
2914         for (const auto& e : accountData) {
2915             if (!e.second->contentJson().isEmpty())
2916                 accountDataEvents.append(e.second->fullJson());
2917         }
2918         result.insert(QStringLiteral("account_data"),
2919                       QJsonObject {
2920                           { QStringLiteral("events"), accountDataEvents } });
2921     }
2922 
2923     if (const auto& readReceiptEventId = lastReadEventIds.value(q->localUser());
2924         !readReceiptEventId.isEmpty()) //
2925     {
2926         // Okay, that's a mouthful; but basically, it's simply placing an m.read
2927         // event in the 'ephemeral' section of the cached sync payload.
2928         // See also receiptevent.* and m.read example in the spec.
2929         // Only the local user's read receipt is saved - others' are really
2930         // considered ephemeral but this one is useful in understanding where
2931         // the user is in the timeline before any history is loaded.
2932         result.insert(
2933             QStringLiteral("ephemeral"),
2934             QJsonObject {
2935                 { QStringLiteral("events"),
2936                   QJsonArray { QJsonObject {
2937                       { TypeKey, ReceiptEvent::matrixTypeId() },
2938                       { ContentKey,
2939                         QJsonObject {
2940                             { readReceiptEventId,
2941                               QJsonObject {
2942                                   { QStringLiteral("m.read"),
2943                                     QJsonObject {
2944                                         { connection->userId(),
2945                                           QJsonObject {} } } } } } } } } } } });
2946     }
2947 
2948     QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey,
2949                                    unreadMessages } };
2950 
2951     if (highlightCount > 0)
2952         unreadNotifObj.insert(QStringLiteral("highlight_count"), highlightCount);
2953     if (notificationCount > 0)
2954         unreadNotifObj.insert(QStringLiteral("notification_count"),
2955                               notificationCount);
2956 
2957     result.insert(QStringLiteral("unread_notifications"), unreadNotifObj);
2958 
2959     if (et.elapsed() > 30)
2960         qCDebug(PROFILER) << "Room::toJson() for" << q->objectName() << "took"
2961                           << et;
2962 
2963     return result;
2964 }
2965 
toJson() const2966 QJsonObject Room::toJson() const { return d->toJson(); }
2967 
memberSorter() const2968 MemberSorter Room::memberSorter() const { return MemberSorter(this); }
2969 
operator ()(User * u1,User * u2) const2970 bool MemberSorter::operator()(User* u1, User* u2) const
2971 {
2972     return operator()(u1, room->roomMembername(u2));
2973 }
2974 
operator ()(User * u1,const QString & u2name) const2975 bool MemberSorter::operator()(User* u1, const QString& u2name) const
2976 {
2977     auto n1 = room->roomMembername(u1);
2978     if (n1.startsWith('@'))
2979         n1.remove(0, 1);
2980     auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0);
2981 
2982     return n1.localeAwareCompare(n2) < 0;
2983 }
2984