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