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