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