aboutsummaryrefslogtreecommitdiff
path: root/lib/room.cpp
diff options
context:
space:
mode:
authorn-peugnet <n.peugnet@free.fr>2022-10-06 19:27:24 +0200
committern-peugnet <n.peugnet@free.fr>2022-10-06 19:27:24 +0200
commit08632625e1a04257b5c7d4a9db2246ac07436748 (patch)
tree9ddadf219a7e352ddd3549ad1683282c944adfb6 /lib/room.cpp
parente9c2e2a26d3711e755aa5eb8a8478917c71d612b (diff)
parentd911b207f49e936b3e992200796110f0749ed150 (diff)
downloadlibquotient-08632625e1a04257b5c7d4a9db2246ac07436748.tar.gz
libquotient-08632625e1a04257b5c7d4a9db2246ac07436748.zip
Update upstream source from tag 'upstream/0.7.0'
Update to upstream version '0.7.0' with Debian dir 30dcb77a77433e4a54eab77c0b82ae925dead2d8
Diffstat (limited to 'lib/room.cpp')
-rw-r--r--lib/room.cpp2164
1 files changed, 1350 insertions, 814 deletions
diff --git a/lib/room.cpp b/lib/room.cpp
index 7631abe1..0cf818ce 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -1,37 +1,33 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name>
+// SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com>
+// SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com>
+// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org>
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-FileCopyrightText: 2020 Ram Nad <ramnad1999@gmail.com>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "room.h"
#include "avatar.h"
#include "connection.h"
#include "converters.h"
-#include "e2ee.h"
#include "syncdata.h"
#include "user.h"
+#include "eventstats.h"
+#include "roomstateview.h"
+#include "qt_connection_util.h"
+
+// NB: since Qt 6, moc_room.cpp needs User fully defined
+#include "moc_room.cpp"
#include "csapi/account-data.h"
#include "csapi/banning.h"
#include "csapi/inviting.h"
#include "csapi/kicking.h"
#include "csapi/leaving.h"
-#include "csapi/receipts.h"
#include "csapi/read_markers.h"
+#include "csapi/receipts.h"
#include "csapi/redaction.h"
#include "csapi/room_send.h"
#include "csapi/room_state.h"
@@ -39,28 +35,24 @@
#include "csapi/rooms.h"
#include "csapi/tags.h"
-#include "events/callanswerevent.h"
-#include "events/callcandidatesevent.h"
-#include "events/callhangupevent.h"
-#include "events/callinviteevent.h"
+#include "events/callevents.h"
#include "events/encryptionevent.h"
#include "events/reactionevent.h"
#include "events/receiptevent.h"
#include "events/redactionevent.h"
#include "events/roomavatarevent.h"
+#include "events/roomcanonicalaliasevent.h"
#include "events/roomcreateevent.h"
#include "events/roommemberevent.h"
+#include "events/roompowerlevelsevent.h"
#include "events/roomtombstoneevent.h"
#include "events/simplestateevents.h"
#include "events/typingevent.h"
-#include "events/roompowerlevelsevent.h"
#include "jobs/downloadfilejob.h"
#include "jobs/mediathumbnailjob.h"
-#include "events/roomcanonicalaliasevent.h"
#include <QtCore/QDir>
#include <QtCore/QHash>
-#include <QtCore/QMimeDatabase>
#include <QtCore/QPointer>
#include <QtCore/QRegularExpression>
#include <QtCore/QStringBuilder> // for efficient string concats (operator%)
@@ -71,13 +63,15 @@
#include <functional>
#ifdef Quotient_E2EE_ENABLED
-#include <account.h> // QtOlm
-#include <errors.h> // QtOlm
-#include <groupsession.h> // QtOlm
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmaccount.h"
+#include "e2ee/qolminboundsession.h"
+#include "e2ee/qolmutility.h"
+#include "database.h"
#endif // Quotient_E2EE_ENABLED
+
using namespace Quotient;
-using namespace QtOlm;
using namespace std::placeholders;
using std::move;
#if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123)
@@ -109,7 +103,7 @@ public:
static decltype(baseState) stubbedState;
/// The state of the room at syncEdge()
/// \sa syncEdge
- QHash<StateEventKey, const StateEventBase*> currentState;
+ RoomStateView currentState;
/// Servers with aliases for this room except the one of the local user
/// \sa Room::remoteAliases
QSet<QString> aliasServers;
@@ -120,27 +114,31 @@ public:
// A map from evtId to a map of relation type to a vector of event
// pointers. Not using QMultiHash, because we want to quickly return
// a number of relations for a given event without enumerating them.
- QHash<QPair<QString, QString>, RelatedEvents> relations;
+ QHash<std::pair<QString, QString>, RelatedEvents> relations;
QString displayname;
Avatar avatar;
- int highlightCount = 0;
- int notificationCount = 0;
+ QHash<QString, Notification> notifications;
+ qsizetype serverHighlightCount = 0;
+ // Starting up with estimate event statistics as there's zero knowledge
+ // about the timeline.
+ EventStats partiallyReadStats {}, unreadStats {};
members_map_t membersMap;
QList<User*> usersTyping;
- QMultiHash<QString, User*> eventIdReadUsers;
+ QHash<QString, QSet<QString>> eventIdReadUsers;
QList<User*> usersInvited;
QList<User*> membersLeft;
- int unreadMessages = 0;
bool displayed = false;
QString firstDisplayedEventId;
QString lastDisplayedEventId;
- QHash<const User*, QString> lastReadEventIds;
+ QHash<QString, ReadReceipt> lastReadReceipts;
QString fullyReadUntilEventId;
TagsMap tags;
UnorderedMap<QString, EventPtr> accountData;
QString prevBatch;
QPointer<GetRoomEventsJob> eventsHistoryJob;
QPointer<GetMembersByRoomJob> allMembersJob;
+ // Map from megolm sessionId to set of eventIds
+ UnorderedMap<QString, QSet<QString>> undecryptedEvents;
struct FileTransferPrivateInfo {
FileTransferPrivateInfo() = default;
@@ -207,9 +205,9 @@ public:
rev_iter_t historyEdge() const { return timeline.crend(); }
Timeline::const_iterator syncEdge() const { return timeline.cend(); }
- void getPreviousContent(int limit = 10);
+ void getPreviousContent(int limit = 10, const QString &filter = {});
- const StateEventBase* getCurrentState(const StateEventKey& evtKey) const
+ const StateEvent* getCurrentState(const StateEventKey& evtKey) const
{
const auto* evt = currentState.value(evtKey, nullptr);
if (!evt) {
@@ -217,10 +215,11 @@ public:
// In the absence of a real event, make a stub as-if an event
// with empty content has been received. Event classes should be
// prepared for empty/invalid/malicious content anyway.
- stubbedState.emplace(evtKey, loadStateEvent(evtKey.first, {},
- evtKey.second));
+ stubbedState.emplace(
+ evtKey, loadEvent<StateEvent>(evtKey.first, evtKey.second));
qCDebug(STATE) << "A new stub event created for key {"
<< evtKey.first << evtKey.second << "}";
+ qCDebug(STATE) << "Stubbed state size:" << stubbedState.size();
}
evt = stubbedState[evtKey].get();
Q_ASSERT(evt);
@@ -230,61 +229,20 @@ public:
return evt;
}
- template <typename EventT>
- const EventT* getCurrentState(const QString& stateKey = {}) const
- {
- const StateEventKey evtKey { EventT::matrixTypeId(), stateKey };
- const auto* evt = currentState.value(evtKey, nullptr);
- if (!evt) {
- if (stubbedState.find(evtKey) == stubbedState.end()) {
- // In the absence of a real event, make a stub as-if an event
- // with empty content has been received. Event classes should be
- // prepared for empty/invalid/malicious content anyway.
- stubbedState.emplace(
- evtKey, makeEvent<EventT>(basicStateEventJson(
- EventT::matrixTypeId(), {}, evtKey.second)));
- qCDebug(STATE) << "A new stub event created for key {"
- << evtKey.first << evtKey.second << "}";
- }
- evt = stubbedState[evtKey].get();
- Q_ASSERT(evt);
- }
- Q_ASSERT(evt->type() == EventT::typeId()
- && evt->matrixType() == EventT::matrixTypeId()
- && evt->stateKey() == stateKey);
- return static_cast<const EventT*>(evt);
- }
-
-// template <typename EventT>
-// const auto& getCurrentStateContent(const QString& stateKey = {}) const
-// {
-// if (const auto* evt =
-// currentState.value({ EventT::matrixTypeId(), stateKey }, nullptr))
-// return evt->content();
-// return EventT::content_type()
-// }
-
- bool isEventNotable(const TimelineItem& ti) const
- {
- return !ti->isRedacted() && ti->senderId() != connection->userId()
- && is<RoomMessageEvent>(*ti)
- && ti.viewAs<RoomMessageEvent>()->replacedEvent().isEmpty();
- }
-
template <typename EventArrayT>
Changes updateStateFrom(EventArrayT&& events)
{
- Changes changes = NoChange;
+ Changes changes {};
if (!events.empty()) {
QElapsedTimer et;
et.start();
for (auto&& eptr : events) {
const auto& evt = *eptr;
Q_ASSERT(evt.isStateEvent());
- // Update baseState afterwards to make sure that the old state
- // is valid and usable inside processStateEvent
- changes |= q->processStateEvent(evt);
- baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr);
+ if (auto change = q->processStateEvent(evt); change) {
+ changes |= change;
+ baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr);
+ }
}
if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
qCDebug(PROFILER)
@@ -296,6 +254,9 @@ public:
Changes addNewMessageEvents(RoomEvents&& events);
void addHistoricalMessageEvents(RoomEvents&& events);
+ Changes updateStatsFromSyncData(const SyncRoomData &data, bool fromCache);
+ void postprocessChanges(Changes changes, bool saveState = true);
+
/** Move events into the timeline
*
* Insert events into the timeline, either new or historical.
@@ -312,13 +273,20 @@ public:
* Remove events from the passed container that are already in the timeline
*/
void dropDuplicateEvents(RoomEvents& events) const;
-
- void setLastReadReceipt(User* u, rev_iter_t newMarker,
- QString newEvtId = {});
+ void decryptIncomingEvents(RoomEvents& events);
+
+ //! \brief update last receipt record for a given user
+ //!
+ //! \return previous event id of the receipt if the new receipt changed
+ //! it, or `none` if no change took place
+ Omittable<QString> setLastReadReceipt(const QString& userId, rev_iter_t newMarker,
+ ReadReceipt newReceipt = {});
+ Changes setLocalLastReadReceipt(const rev_iter_t& newMarker,
+ ReadReceipt newReceipt = {},
+ bool deferStatsUpdate = false);
Changes setFullyReadMarker(const QString &eventId);
- Changes updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to);
- Changes recalculateUnreadCount(bool force = false);
- void markMessagesAsRead(const rev_iter_t &upToMarker);
+ Changes updateStats(const rev_iter_t& from, const rev_iter_t& to);
+ bool markMessagesAsRead(const rev_iter_t& upToMarker);
void getAllMembers();
@@ -330,12 +298,16 @@ public:
return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...));
}
+ QString doPostFile(RoomEventPtr &&msgEvent, const QUrl &localUrl);
+
RoomEvent* addAsPending(RoomEventPtr&& event);
QString doSendEvent(const RoomEvent* pEvent);
void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr);
- SetRoomStateWithKeyJob* requestSetState(const StateEventBase& event)
+ SetRoomStateWithKeyJob* requestSetState(const QString& evtType,
+ const QString& stateKey,
+ const QJsonObject& contentJson)
{
// if (event.roomId().isEmpty())
// event.setRoomId(id);
@@ -343,14 +315,8 @@ public:
// event.setSender(connection->userId());
// TODO: Queue up state events sending (see #133).
// TODO: Maybe addAsPending() as well, despite having no txnId
- return connection->callApi<SetRoomStateWithKeyJob>(
- id, event.matrixType(), event.stateKey(), event.contentJson());
- }
-
- template <typename EvT, typename... ArgTs>
- auto requestSetState(ArgTs&&... args)
- {
- return requestSetState(EvT(std::forward<ArgTs>(args)...));
+ return connection->callApi<SetRoomStateWithKeyJob>(id, evtType, stateKey,
+ contentJson);
}
/*! Apply redaction to the timeline
@@ -376,87 +342,122 @@ public:
bool isLocalUser(const User* u) const { return u == q->localUser(); }
#ifdef Quotient_E2EE_ENABLED
- // A map from <sessionId, messageIndex> to <event_id, origin_server_ts>
- QHash<QPair<QString, uint32_t>, QPair<QString, QDateTime>>
- groupSessionIndexRecord; // TODO: cache
- // A map from senderKey to a map of sessionId to InboundGroupSession
- // Not using QMultiHash, because we want to quickly return
- // a number of relations for a given event without enumerating them.
- QHash<QPair<QString, QString>, InboundGroupSession*> groupSessions; // TODO:
- // cache
- bool addInboundGroupSession(QString senderKey, QString sessionId,
- QString sessionKey)
+ UnorderedMap<QString, QOlmInboundGroupSessionPtr> groupSessions;
+ int currentMegolmSessionMessageCount = 0;
+ //TODO save this to database
+ unsigned long long currentMegolmSessionCreationTimestamp = 0;
+ QOlmOutboundGroupSessionPtr currentOutboundMegolmSession = nullptr;
+
+ bool addInboundGroupSession(QString sessionId, QByteArray sessionKey,
+ const QString& senderId,
+ const QString& olmSessionId)
{
- if (groupSessions.contains({ senderKey, sessionId })) {
- qCDebug(E2EE) << "Inbound Megolm session" << sessionId
- << "with senderKey" << senderKey << "already exists";
+ if (groupSessions.contains(sessionId)) {
+ qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "already exists";
return false;
}
- InboundGroupSession* megolmSession;
- try {
- megolmSession = new InboundGroupSession(sessionKey.toLatin1(),
- InboundGroupSession::Init,
- q);
- } catch (OlmError* e) {
- qCDebug(E2EE) << "Unable to create new InboundGroupSession"
- << e->what();
- return false;
- }
- if (megolmSession->id() != sessionId) {
- qCDebug(E2EE) << "Session ID mismatch in m.room_key event sent "
- "from sender with key"
- << senderKey;
+ auto expectedMegolmSession = QOlmInboundGroupSession::create(sessionKey);
+ Q_ASSERT(expectedMegolmSession.has_value());
+ auto&& megolmSession = *expectedMegolmSession;
+ if (megolmSession->sessionId() != sessionId) {
+ qCWarning(E2EE) << "Session ID mismatch in m.room_key event";
return false;
}
- groupSessions.insert({ senderKey, sessionId }, megolmSession);
+ megolmSession->setSenderId(senderId);
+ megolmSession->setOlmSessionId(olmSessionId);
+ qCWarning(E2EE) << "Adding inbound session";
+ connection->saveMegolmSession(q, *megolmSession);
+ groupSessions[sessionId] = std::move(megolmSession);
return true;
}
QString groupSessionDecryptMessage(QByteArray cipher,
- const QString& senderKey,
const QString& sessionId,
const QString& eventId,
- QDateTime timestamp)
+ QDateTime timestamp,
+ const QString& senderId)
{
- std::pair<QString, uint32_t> decrypted;
- QPair<QString, QString> senderSessionPairKey =
- qMakePair(senderKey, sessionId);
- if (!groupSessions.contains(senderSessionPairKey)) {
- qCDebug(E2EE) << "Unable to decrypt event" << eventId
- << "The sender's device has not sent us the keys for "
- "this message";
- return QString();
+ auto groupSessionIt = groupSessions.find(sessionId);
+ if (groupSessionIt == groupSessions.end()) {
+ // qCWarning(E2EE) << "Unable to decrypt event" << eventId
+ // << "The sender's device has not sent us the keys for "
+ // "this message";
+ return {};
}
- InboundGroupSession* senderSession =
- groupSessions.value(senderSessionPairKey);
- if (!senderSession) {
- qCDebug(E2EE) << "Unable to decrypt event" << eventId
- << "senderSessionPairKey:" << senderSessionPairKey;
- return QString();
+ auto& senderSession = groupSessionIt->second;
+ if (senderSession->senderId() != senderId) {
+ qCWarning(E2EE) << "Sender from event does not match sender from session";
+ return {};
}
- try {
- decrypted = senderSession->decrypt(cipher);
- } catch (OlmError* e) {
- qCDebug(E2EE) << "Unable to decrypt event" << eventId
- << "with matching megolm session:" << e->what();
- return QString();
+ auto decryptResult = senderSession->decrypt(cipher);
+ if(!decryptResult) {
+ qCWarning(E2EE) << "Unable to decrypt event" << eventId
+ << "with matching megolm session:" << decryptResult.error();
+ return {};
}
- QPair<QString, QDateTime> properties = groupSessionIndexRecord.value(
- qMakePair(senderSession->id(), decrypted.second));
- if (properties.first.isEmpty()) {
- groupSessionIndexRecord.insert(qMakePair(senderSession->id(),
- decrypted.second),
- qMakePair(eventId, timestamp));
+ const auto& [content, index] = *decryptResult;
+ const auto& [recordEventId, ts] =
+ q->connection()->database()->groupSessionIndexRecord(
+ q->id(), senderSession->sessionId(), index);
+ if (recordEventId.isEmpty()) {
+ q->connection()->database()->addGroupSessionIndexRecord(
+ q->id(), senderSession->sessionId(), index, eventId,
+ timestamp.toMSecsSinceEpoch());
} else {
- if ((properties.first != eventId)
- || (properties.second != timestamp)) {
- qCDebug(E2EE) << "Detected a replay attack on event" << eventId;
- return QString();
+ if ((eventId != recordEventId)
+ || (ts != timestamp.toMSecsSinceEpoch())) {
+ qCWarning(E2EE) << "Detected a replay attack on event" << eventId;
+ return {};
}
}
+ return content;
+ }
+
+ bool shouldRotateMegolmSession() const
+ {
+ const auto* encryptionConfig = currentState.get<EncryptionEvent>();
+ if (!encryptionConfig || !encryptionConfig->useEncryption())
+ return false;
+
+ const auto rotationInterval = encryptionConfig->rotationPeriodMs();
+ const auto rotationMessageCount = encryptionConfig->rotationPeriodMsgs();
+ return currentOutboundMegolmSession->messageCount()
+ >= rotationMessageCount
+ || currentOutboundMegolmSession->creationTime().addMSecs(
+ rotationInterval)
+ < QDateTime::currentDateTime();
+ }
+
+ bool hasValidMegolmSession() const
+ {
+ if (!q->usesEncryption()) {
+ return false;
+ }
+ return currentOutboundMegolmSession != nullptr;
+ }
+
+ void createMegolmSession() {
+ qCDebug(E2EE) << "Creating new outbound megolm session for room "
+ << q->objectName();
+ currentOutboundMegolmSession = QOlmOutboundGroupSession::create();
+ connection->saveCurrentOutboundMegolmSession(
+ id, *currentOutboundMegolmSession);
- return decrypted.first;
+ addInboundGroupSession(currentOutboundMegolmSession->sessionId(),
+ currentOutboundMegolmSession->sessionKey(),
+ q->localUser()->id(), "SELF"_ls);
+ }
+
+ QMultiHash<QString, QString> getDevicesWithoutKey() const
+ {
+ QMultiHash<QString, QString> devices;
+ for (const auto& user : q->users())
+ for (const auto& deviceId : connection->devicesForUser(user->id()))
+ devices.insert(user->id(), deviceId);
+
+ return connection->database()->devicesWithoutKey(
+ id, devices, currentOutboundMegolmSession->sessionId());
}
#endif // Quotient_E2EE_ENABLED
@@ -477,12 +478,37 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
// https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/
d->q = this;
d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name
- connectUntil(connection, &Connection::loadedRoomState, this, [this](Room* r) {
- if (this == r)
- emit baseStateLoaded();
- return this == r; // loadedRoomState fires only once per room
+#ifdef Quotient_E2EE_ENABLED
+ connectSingleShot(this, &Room::encryption, this, [this, connection](){
+ connection->encryptionUpdate(this);
+ });
+ connect(this, &Room::userAdded, this, [this, connection](){
+ if(usesEncryption()) {
+ connection->encryptionUpdate(this);
+ }
+ });
+ d->groupSessions = connection->loadRoomMegolmSessions(this);
+ d->currentOutboundMegolmSession =
+ connection->loadCurrentOutboundMegolmSession(this->id());
+ if (d->shouldRotateMegolmSession()) {
+ d->currentOutboundMegolmSession = nullptr;
+ }
+ connect(this, &Room::userRemoved, this, [this](){
+ if (!usesEncryption()) {
+ return;
+ }
+ if (d->hasValidMegolmSession()) {
+ d->createMegolmSession();
+ }
+ qCDebug(E2EE) << "Invalidating current megolm session because user left";
+
});
- qCDebug(STATE) << "New" << toCString(initialJoinState) << "Room:" << id;
+
+ connect(this, &Room::beforeDestruction, this, [=](){
+ connection->database()->clearRoomData(id);
+ });
+#endif
+ qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id;
}
Room::~Room() { delete d; }
@@ -491,8 +517,8 @@ const QString& Room::id() const { return d->id; }
QString Room::version() const
{
- const auto v = d->getCurrentState<RoomCreateEvent>()->version();
- return v.isEmpty() ? QStringLiteral("1") : v;
+ const auto v = currentState().query(&RoomCreateEvent::version);
+ return v && !v->isEmpty() ? *v : QStringLiteral("1");
}
bool Room::isUnstable() const
@@ -503,7 +529,10 @@ bool Room::isUnstable() const
QString Room::predecessorId() const
{
- return d->getCurrentState<RoomCreateEvent>()->predecessor().roomId;
+ if (const auto* evt = currentState().get<RoomCreateEvent>())
+ return evt->predecessor().roomId;
+
+ return {};
}
Room* Room::predecessor(JoinStates statesFilter) const
@@ -518,7 +547,8 @@ Room* Room::predecessor(JoinStates statesFilter) const
QString Room::successorId() const
{
- return d->getCurrentState<RoomTombstoneEvent>()->successorRoomId();
+ return currentState().queryOr(&RoomTombstoneEvent::successorRoomId,
+ QString());
}
Room* Room::successor(JoinStates statesFilter) const
@@ -545,50 +575,56 @@ bool Room::allHistoryLoaded() const
QString Room::name() const
{
- return d->getCurrentState<RoomNameEvent>()->name();
+ return currentState().content<RoomNameEvent>().value;
}
QStringList Room::aliases() const
{
- const auto* evt = d->getCurrentState<RoomCanonicalAliasEvent>();
- auto result = evt->altAliases();
- if (!evt->alias().isEmpty())
- result << evt->alias();
- return result;
+ if (const auto* evt = currentState().get<RoomCanonicalAliasEvent>()) {
+ auto result = evt->altAliases();
+ if (!evt->alias().isEmpty())
+ result << evt->alias();
+ return result;
+ }
+ return {};
}
QStringList Room::altAliases() const
{
- return d->getCurrentState<RoomCanonicalAliasEvent>()->altAliases();
+ return currentState().content<RoomCanonicalAliasEvent>().altAliases;
}
-QStringList Room::localAliases() const
+QString Room::canonicalAlias() const
{
- return d->getCurrentState<RoomAliasesEvent>(
- connection()->domain())
- ->aliases();
+ return currentState().queryOr(&RoomCanonicalAliasEvent::alias, QString());
}
-QStringList Room::remoteAliases() const
-{
- QStringList result;
- for (const auto& s : std::as_const(d->aliasServers))
- result += d->getCurrentState<RoomAliasesEvent>(s)->aliases();
- return result;
+QString Room::displayName() const { return d->displayname; }
+
+QStringList Room::pinnedEventIds() const {
+ return currentState().queryOr(&RoomPinnedEvent::pinnedEvents, QStringList());
}
-QString Room::canonicalAlias() const
+QVector<const Quotient::RoomEvent*> Quotient::Room::pinnedEvents() const
{
- return d->getCurrentState<RoomCanonicalAliasEvent>()->alias();
+ QVector<const RoomEvent*> pinnedEvents;
+ for (const auto& evtId : pinnedEventIds())
+ if (const auto& it = findInTimeline(evtId); it != historyEdge())
+ pinnedEvents.append(it->event());
+
+ return pinnedEvents;
}
-QString Room::displayName() const { return d->displayname; }
+QString Room::displayNameForHtml() const
+{
+ return displayName().toHtmlEscaped();
+}
void Room::refreshDisplayName() { d->updateDisplayname(); }
QString Room::topic() const
{
- return d->getCurrentState<RoomTopicEvent>()->topic();
+ return currentState().queryOr(&RoomTopicEvent::topic, QString());
}
QString Room::avatarMediaId() const { return d->avatar.mediaId(); }
@@ -603,13 +639,13 @@ QImage Room::avatar(int width, int height)
{
if (!d->avatar.url().isEmpty())
return d->avatar.get(connection(), width, height,
- [=] { emit avatarChanged(); });
+ [this] { emit avatarChanged(); });
// Use the first (excluding self) user's avatar for direct chats
const auto dcUsers = directChatUsers();
for (auto* u : dcUsers)
if (u != localUser())
- return u->avatar(width, height, this, [=] { emit avatarChanged(); });
+ return u->avatar(width, height, this, [this] { emit avatarChanged(); });
return {};
}
@@ -621,9 +657,19 @@ User* Room::user(const QString& userId) const
JoinState Room::memberJoinState(User* user) const
{
- return user != nullptr && d->membersMap.contains(user->name(this), user)
- ? JoinState::Join
- : JoinState::Leave;
+ return d->membersMap.contains(user->name(this), user) ? JoinState::Join
+ : JoinState::Leave;
+}
+
+Membership Room::memberState(const QString& userId) const
+{
+ return currentState().queryOr(userId, &RoomMemberEvent::membership,
+ Membership::Leave);
+}
+
+bool Room::isMember(const QString& userId) const
+{
+ return memberState(userId) == Membership::Join;
}
JoinState Room::joinState() const { return d->joinState; }
@@ -634,195 +680,277 @@ void Room::setJoinState(JoinState state)
if (state == oldState)
return;
d->joinState = state;
- qCDebug(STATE) << "Room" << id() << "changed state: " << int(oldState)
- << "->" << int(state);
- emit changed(Change::JoinStateChange);
+ qCDebug(STATE) << "Room" << id() << "changed state: " << terse << oldState
+ << "->" << state;
emit joinStateChanged(oldState, state);
}
-void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker,
- QString newEvtId)
+Omittable<QString> Room::Private::setLastReadReceipt(const QString& userId,
+ rev_iter_t newMarker,
+ ReadReceipt newReceipt)
{
- if (!u) {
- Q_ASSERT(u != nullptr); // For Debug builds
- qCCritical(MAIN) << "Empty user, skipping read receipt registration";
- return; // For Release builds
- }
- if (q->memberJoinState(u) != JoinState::Join) {
- qCWarning(EPHEMERAL)
- << "Won't record read receipt for non-member" << u->id();
- return;
- }
-
- if (newMarker == historyEdge() && !newEvtId.isEmpty())
- newMarker = q->findInTimeline(newEvtId);
+ if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty())
+ newMarker = q->findInTimeline(newReceipt.eventId);
if (newMarker != historyEdge()) {
- // NB: with reverse iterators, timeline history >= sync edge
- if (newMarker >= q->readMarker(u)) {
- qCDebug(EPHEMERAL) << "The new read receipt for" << u->id()
- << "is at or behind the old one, skipping";
- return;
- }
-
// Try to auto-promote the read marker over the user's own messages
// (switch to direct iterators for that).
const auto eagerMarker = find_if(newMarker.base(), syncEdge(),
[=](const TimelineItem& ti) {
- return ti->senderId() != u->id();
- })
- - 1;
- newEvtId = (*eagerMarker)->id();
- if (eagerMarker != newMarker.base() - 1) // &*(rIt.base() - 1) === &*rIt
- qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << u->id()
- << "to" << newEvtId;
- }
+ return ti->senderId() != userId;
+ });
+ // eagerMarker is now just after the desired event for newMarker
+ if (eagerMarker != newMarker.base()) {
+ newMarker = rev_iter_t(eagerMarker);
+ qDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId
+ << "to" << *newMarker;
+ }
+ // Fill newReceipt with the event (and, if needed, timestamp) from
+ // eagerMarker
+ newReceipt.eventId = (eagerMarker - 1)->event()->id();
+ if (newReceipt.timestamp.isNull())
+ newReceipt.timestamp = QDateTime::currentDateTime();
+ }
+ auto& storedReceipt =
+ lastReadReceipts[userId]; // clazy:exclude=detaching-member
+ const auto prevEventId = storedReceipt.eventId;
+ // Check that either the new marker is actually "newer" than the current one
+ // or, if both markers are at historyEdge(), event ids are different.
+ // This logic tackles, in particular, the case when the new event is not
+ // found (most likely, because it's too old and hasn't been fetched from
+ // the server yet) but there is a previous marker for a user; in that case,
+ // the previous marker is kept because read receipts are not supposed
+ // to move backwards. If neither new nor old event is found, the new receipt
+ // is blindly stored, in a hope it's also "newer" in the timeline.
+ // NB: with reverse iterators, timeline history edge >= sync edge
+ if (prevEventId == newReceipt.eventId
+ || newMarker > q->findInTimeline(prevEventId))
+ return {};
- auto& storedId = lastReadEventIds[u];
- if (storedId == newEvtId)
- return;
// Finally make the change
- eventIdReadUsers.remove(storedId, u);
- eventIdReadUsers.insert(newEvtId, u);
- swap(storedId, newEvtId); // Now newEvtId actually stores the old eventId
- qCDebug(EPHEMERAL) << "The new read receipt for" << u->id() << "is at"
- << storedId;
- emit q->lastReadEventChanged(u);
- if (!isLocalUser(u))
- emit q->readMarkerForUserMoved(u, newEvtId, storedId);
+
+ auto oldEventReadUsersIt =
+ eventIdReadUsers.find(prevEventId); // clazy:exclude=detaching-member
+ if (oldEventReadUsersIt != eventIdReadUsers.end()) {
+ oldEventReadUsersIt->remove(userId);
+ if (oldEventReadUsersIt->isEmpty())
+ eventIdReadUsers.erase(oldEventReadUsersIt);
+ }
+ eventIdReadUsers[newReceipt.eventId].insert(userId);
+ storedReceipt = move(newReceipt);
+
+ {
+ auto dbg = qDebug(EPHEMERAL); // NB: qCDebug can't be used like that
+ dbg << "The new read receipt for" << userId << "is now at";
+ if (newMarker == historyEdge())
+ dbg << storedReceipt.eventId;
+ else
+ dbg << *newMarker;
+ }
+
+ // NB: This method, unlike setLocalLastReadReceipt, doesn't emit
+ // lastReadEventChanged() to avoid numerous emissions when many read
+ // receipts arrive. It can be called thousands of times during an initial
+ // sync, e.g.
+ // TODO: remove in 0.8
+ if (const auto member = q->user(userId); !isLocalUser(member))
+ QT_IGNORE_DEPRECATIONS(emit q->readMarkerForUserMoved(
+ member, prevEventId, storedReceipt.eventId);)
+ return prevEventId;
+}
+
+Room::Changes Room::Private::setLocalLastReadReceipt(const rev_iter_t& newMarker,
+ ReadReceipt newReceipt,
+ bool deferStatsUpdate)
+{
+ auto prevEventId =
+ setLastReadReceipt(connection->userId(), newMarker, move(newReceipt));
+ if (!prevEventId)
+ return Change::None;
+ Changes changes = Change::Other;
+ if (!deferStatsUpdate) {
+ if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(*prevEventId),
+ newMarker)) {
+ qDebug(MESSAGES)
+ << "Updated unread event statistics in" << q->objectName()
+ << "after moving the local read receipt:" << unreadStats;
+ changes |= Change::UnreadStats;
+ }
+ Q_ASSERT(unreadStats.isValidFor(q, newMarker)); // post-check
+ }
+ emit q->lastReadEventChanged({ connection->userId() });
+ return changes;
}
-Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from,
- const rev_iter_t& to)
+Room::Changes Room::Private::updateStats(const rev_iter_t& from,
+ const rev_iter_t& to)
{
Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend());
Q_ASSERT(to >= from && to <= timeline.crend());
- auto fullyReadMarker = q->readMarker();
+ const auto fullyReadMarker = q->fullyReadMarker();
+ auto readReceiptMarker = q->localReadReceiptMarker();
+ Changes changes = Change::None;
+ // Correct the read receipt to never be behind the fully read marker
+ if (readReceiptMarker > fullyReadMarker
+ && setLocalLastReadReceipt(fullyReadMarker, {}, true)) {
+ changes |= Change::Other;
+ readReceiptMarker = q->localReadReceiptMarker();
+ qCInfo(MESSAGES) << "The local m.read receipt was behind m.fully_read "
+ "marker - it's now corrected to be at index"
+ << readReceiptMarker->index();
+ }
+
if (fullyReadMarker < from)
- return NoChange; // What's arrived is already fully read
+ return Change::None; // What's arrived is already fully read
// If there's no read marker in the whole room, initialise it
if (fullyReadMarker == historyEdge() && q->allHistoryLoaded())
return setFullyReadMarker(timeline.front()->id());
- // Catch a special case when the last fully read event id refers to an
- // event that has just arrived. In this case we should recalculate
- // unreadMessages to get an exact number instead of an estimation
- // (see https://github.com/quotient-im/libQuotient/wiki/unread_count).
- // For the same reason (switching from the estimation to the exact
- // number) this branch always emits unreadMessagesChanged() and returns
- // UnreadNotifsChange, even if the estimation luckily matched the exact
- // result.
- if (fullyReadMarker < to)
- return recalculateUnreadCount(true);
-
- // At this point the fully read marker is somewhere beyond the "oldest"
- // message from the arrived batch - add up newly arrived messages to
- // the current counter, instead of a complete recalculation.
- Q_ASSERT(to <= fullyReadMarker);
+ // Catch a case when the id in the last fully read marker or the local read
+ // receipt refers to an event that has just arrived. In this case either
+ // one (unreadStats) or both statistics should be recalculated to get
+ // an exact number instead of an estimation (see documentation on
+ // EventStats::isEstimate). For the same reason (switching from the
+ // estimate to the exact number) this branch forces returning
+ // Change::UnreadStats and also possibly Change::PartiallyReadStats, even if
+ // the estimation luckily matched the exact result.
+ if (readReceiptMarker < to || changes /*i.e. read receipt was corrected*/) {
+ unreadStats = EventStats::fromMarker(q, readReceiptMarker);
+ Q_ASSERT(!unreadStats.isEstimate);
+ qCDebug(MESSAGES).nospace() << "Recalculated unread event statistics in"
+ << q->objectName() << ": " << unreadStats;
+ changes |= Change::UnreadStats;
+ if (fullyReadMarker < to) {
+ // Add up to unreadStats instead of counting same events again
+ partiallyReadStats = EventStats::fromRange(q, readReceiptMarker,
+ q->fullyReadMarker(),
+ unreadStats);
+ Q_ASSERT(!partiallyReadStats.isEstimate);
+
+ qCDebug(MESSAGES).nospace()
+ << "Recalculated partially read event statistics in "
+ << q->objectName() << ": " << partiallyReadStats;
+ return changes | Change::PartiallyReadStats;
+ }
+ }
- QElapsedTimer et;
- et.start();
- const auto newUnreadMessages =
- count_if(from, to,
- std::bind(&Room::Private::isEventNotable, this, _1));
- if (et.nsecsElapsed() > profilerMinNsecs() / 10)
- qCDebug(PROFILER) << "Counting gained unread messages in"
- << q->objectName() << "took" << et;
-
- if (newUnreadMessages == 0)
- return NoChange;
-
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (unreadMessages < 0)
- unreadMessages = 0;
-
- unreadMessages += newUnreadMessages;
- qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained"
- << newUnreadMessages << "unread message(s),"
- << (q->readMarker() == historyEdge()
- ? "in total at least"
- : "in total")
- << unreadMessages << "unread message(s)";
- emit q->unreadMessagesChanged(q);
- return UnreadNotifsChange;
-}
-
-Room::Changes Room::Private::recalculateUnreadCount(bool force)
-{
- // The recalculation logic assumes that the fully read marker points at
- // a specific position in the timeline
- Q_ASSERT(q->readMarker() != historyEdge());
- const auto oldUnreadCount = unreadMessages;
- QElapsedTimer et;
- et.start();
- unreadMessages =
- int(count_if(timeline.crbegin(), q->readMarker(),
- [this](const auto& ti) { return isEventNotable(ti); }));
- if (et.nsecsElapsed() > profilerMinNsecs() / 10)
- qCDebug(PROFILER) << "Recounting unread messages in" << q->objectName()
- << "took" << et;
+ // As of here, at least the fully read marker (but maybe also read receipt)
+ // points to somewhere beyond the "oldest" message from the arrived batch -
+ // add up newly arrived messages to the current stats, instead of a complete
+ // recalculation.
+ Q_ASSERT(fullyReadMarker >= to);
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (unreadMessages == 0)
- unreadMessages = -1;
+ const auto newStats = EventStats::fromRange(q, from, to);
+ Q_ASSERT(!newStats.isEstimate);
+ if (newStats.empty())
+ return changes;
- if (!force && unreadMessages == oldUnreadCount)
- return NoChange;
+ const auto doAddStats = [this, &changes, newStats](EventStats& s,
+ const rev_iter_t& marker,
+ Change c) {
+ s.notableCount += newStats.notableCount;
+ s.highlightCount += newStats.highlightCount;
+ if (!s.isEstimate)
+ s.isEstimate = marker == historyEdge();
+ changes |= c;
+ };
- if (unreadMessages == -1)
- qCDebug(MESSAGES)
- << "Room" << displayname << "has no more unread messages";
- else
- qCDebug(MESSAGES) << "Room" << displayname << "still has"
- << unreadMessages << "unread message(s)";
- emit q->unreadMessagesChanged(q);
- return UnreadNotifsChange;
+ doAddStats(partiallyReadStats, fullyReadMarker, Change::PartiallyReadStats);
+ if (readReceiptMarker >= to) {
+ // readReceiptMarker < to branch shouldn't have been entered
+ Q_ASSERT(!changes.testFlag(Change::UnreadStats));
+ doAddStats(unreadStats, readReceiptMarker, Change::UnreadStats);
+ }
+ qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" << newStats
+ << "notable/highlighted event(s); total statistics:"
+ << partiallyReadStats << "since the fully read marker,"
+ << unreadStats << "since read receipt";
+
+ // Check invariants
+ Q_ASSERT(partiallyReadStats.isValidFor(q, fullyReadMarker));
+ Q_ASSERT(unreadStats.isValidFor(q, readReceiptMarker));
+ return changes;
}
Room::Changes Room::Private::setFullyReadMarker(const QString& eventId)
{
if (fullyReadUntilEventId == eventId)
- return NoChange;
+ return Change::None;
+
+ const auto prevReadMarker = q->fullyReadMarker();
+ const auto newReadMarker = q->findInTimeline(eventId);
+ if (newReadMarker > prevReadMarker)
+ return Change::None;
const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId);
qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() //
<< "set to" << fullyReadUntilEventId;
- emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId);
-
- Changes changes = ReadMarkerChange;
- if (const auto rm = q->readMarker(); rm != historyEdge()) {
- // Pull read receipt if it's behind
- if (auto rr = q->readMarker(q->localUser()); rr > rm)
- setLastReadReceipt(q->localUser(), rm);
- changes |= recalculateUnreadCount();
+ QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker|Change::Other;)
+ if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) {
+ // Pull read receipt if it's behind, and update statistics
+ changes |= setLocalLastReadReceipt(rm);
+ if (partiallyReadStats.updateOnMarkerMove(q, prevReadMarker, rm)) {
+ changes |= Change::PartiallyReadStats;
+ qCDebug(MESSAGES)
+ << "Updated partially read event statistics in"
+ << q->objectName()
+ << "after moving m.fully_read marker: " << partiallyReadStats;
+ }
+ Q_ASSERT(partiallyReadStats.isValidFor(q, rm)); // post-check
}
+ emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId);
+ // TODO: Remove in 0.8
+ QT_IGNORE_DEPRECATIONS(
+ emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId);)
return changes;
}
-void Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker)
+void Room::setReadReceipt(const QString& atEventId)
{
- if (upToMarker < q->readMarker()) {
- setFullyReadMarker((*upToMarker)->id());
- // Assuming that if a read receipt was sent on a newer event, it will
- // stay there instead of "un-reading" notifications/mentions from
- // m.fully_read to m.read
+ if (const auto changes =
+ d->setLocalLastReadReceipt(historyEdge(), { atEventId })) {
+ connection()->callApi<PostReceiptJob>(BackgroundRequest, id(),
+ QStringLiteral("m.read"),
+ QUrl::toPercentEncoding(atEventId));
+ d->postprocessChanges(changes);
+ } else
+ qCDebug(EPHEMERAL) << "The new read receipt for" << localUser()->id()
+ << "in" << objectName()
+ << "is at or behind the old one, skipping";
+}
+
+bool Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker)
+{
+ if (upToMarker == q->historyEdge())
+ qCWarning(MESSAGES) << "Cannot mark an unknown event in"
+ << q->objectName() << "as fully read";
+ else if (const auto changes = setFullyReadMarker(upToMarker->event()->id())) {
+ // The assumption below is that if a read receipt was sent on a newer
+ // event, the homeserver will keep it there instead of reverting to
+ // m.fully_read
connection->callApi<SetReadMarkerJob>(BackgroundRequest, id,
fullyReadUntilEventId,
fullyReadUntilEventId);
- }
+ postprocessChanges(changes);
+ return true;
+ } else
+ qCDebug(MESSAGES) << "Event" << *upToMarker << "in" << q->objectName()
+ << "is behind the current fully read marker at"
+ << *q->fullyReadMarker()
+ << "- won't move fully read marker back in timeline";
+ return false;
}
-void Room::markMessagesAsRead(QString uptoEventId)
+void Room::markMessagesAsRead(const QString& uptoEventId)
{
d->markMessagesAsRead(findInTimeline(uptoEventId));
}
void Room::markAllMessagesAsRead()
{
- if (!d->timeline.empty())
- d->markMessagesAsRead(d->timeline.crbegin());
+ d->markMessagesAsRead(d->timeline.crbegin());
}
bool Room::canSwitchVersions() const
@@ -830,8 +958,9 @@ bool Room::canSwitchVersions() const
if (!successorId().isEmpty())
return false; // No one can upgrade a room that's already upgraded
- if (const auto* plEvt = d->getCurrentState<RoomPowerLevelsEvent>()) {
- const auto currentUserLevel = plEvt->powerLevelForUser(localUser()->id());
+ if (const auto* plEvt = currentState().get<RoomPowerLevelsEvent>()) {
+ const auto currentUserLevel =
+ plEvt->powerLevelForUser(localUser()->id());
const auto tombstonePowerLevel =
plEvt->powerLevelForState("m.room.tombstone"_ls);
return currentUserLevel >= tombstonePowerLevel;
@@ -839,16 +968,45 @@ bool Room::canSwitchVersions() const
return true;
}
-bool Room::hasUnreadMessages() const { return unreadCount() >= 0; }
+bool Room::isEventNotable(const TimelineItem &ti) const
+{
+ const auto& evt = *ti;
+ const auto* rme = ti.viewAs<RoomMessageEvent>();
+ return !evt.isRedacted()
+ && (is<RoomTopicEvent>(evt) || is<RoomNameEvent>(evt)
+ || is<RoomAvatarEvent>(evt) || is<RoomTombstoneEvent>(evt)
+ || (rme && rme->msgtype() != MessageEventType::Notice
+ && rme->replacedEvent().isEmpty()))
+ && evt.senderId() != localUser()->id();
+}
+
+Notification Room::notificationFor(const TimelineItem &ti) const
+{
+ return d->notifications.value(ti->id());
+}
+
+Notification Room::checkForNotifications(const TimelineItem &ti)
+{
+ return { Notification::None };
+}
+
+bool Room::hasUnreadMessages() const { return !d->partiallyReadStats.empty(); }
+
+int countFromStats(const EventStats& s)
+{
+ return s.empty() ? -1 : int(s.notableCount);
+}
+
+int Room::unreadCount() const { return countFromStats(partiallyReadStats()); }
+
+EventStats Room::partiallyReadStats() const { return d->partiallyReadStats; }
-int Room::unreadCount() const { return d->unreadMessages; }
+EventStats Room::unreadStats() const { return d->unreadStats; }
Room::rev_iter_t Room::historyEdge() const { return d->historyEdge(); }
Room::Timeline::const_iterator Room::syncEdge() const { return d->syncEdge(); }
-Room::rev_iter_t Room::timelineEdge() const { return d->historyEdge(); }
-
TimelineItem::index_t Room::minTimelineIndex() const
{
return d->timeline.empty() ? 0 : d->timeline.front().index();
@@ -867,7 +1025,7 @@ bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const
Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const
{
- return timelineEdge()
+ return historyEdge()
- (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0);
}
@@ -898,28 +1056,38 @@ Room::findPendingEvent(const QString& txnId) const
});
}
-const Room::RelatedEvents Room::relatedEvents(const QString& evtId,
- const char* relType) const
+const Room::RelatedEvents Room::relatedEvents(
+ const QString& evtId, EventRelation::reltypeid_t relType) const
{
return d->relations.value({ evtId, relType });
}
-const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt,
- const char* relType) const
+const Room::RelatedEvents Room::relatedEvents(
+ const RoomEvent& evt, EventRelation::reltypeid_t relType) const
{
return relatedEvents(evt.id(), relType);
}
+const RoomCreateEvent* Room::creation() const
+{
+ return currentState().get<RoomCreateEvent>();
+}
+
+const RoomTombstoneEvent *Room::tombstone() const
+{
+ return currentState().get<RoomTombstoneEvent>();
+}
+
void Room::Private::getAllMembers()
{
// If already loaded or already loading, there's nothing to do here.
- if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob))
+ if (q->joinedCount() <= membersMap.size() || isJobPending(allMembersJob))
return;
allMembersJob = connection->callApi<GetMembersByRoomJob>(
id, connection->nextBatchToken(), "join");
auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1;
- connect(allMembersJob, &BaseJob::success, q, [=] {
+ connect(allMembersJob, &BaseJob::success, q, [this, nextIndex] {
Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1);
auto roomChanges = updateStateFrom(allMembersJob->chunk());
// Replay member events that arrived after the point for which
@@ -929,8 +1097,7 @@ void Room::Private::getAllMembers()
it != syncEdge(); ++it)
if (is<RoomMemberEvent>(**it))
roomChanges |= q->processStateEvent(**it);
- if (roomChanges & MembersChange)
- emit q->memberListChanged();
+ postprocessChanges(roomChanges);
emit q->allMembersLoaded();
});
}
@@ -995,12 +1162,6 @@ void Room::setLastDisplayedEventId(const QString& eventId)
d->lastDisplayedEventId = eventId;
emit lastDisplayedEventChanged();
- if (d->displayed && marker < readMarker(localUser())) {
- d->setLastReadReceipt(localUser(), marker);
- connection()->callApi<PostReceiptJob>(BackgroundRequest, id(),
- QStringLiteral("m.read"),
- QUrl::toPercentEncoding(eventId));
- }
}
void Room::setLastDisplayedEvent(TimelineItem::index_t index)
@@ -1012,41 +1173,70 @@ void Room::setLastDisplayedEvent(TimelineItem::index_t index)
Room::rev_iter_t Room::readMarker(const User* user) const
{
Q_ASSERT(user);
- return findInTimeline(d->lastReadEventIds.value(user));
+ return findInTimeline(lastReadReceipt(user->id()).eventId);
+}
+
+Room::rev_iter_t Room::readMarker() const { return fullyReadMarker(); }
+
+QString Room::readMarkerEventId() const { return lastFullyReadEventId(); }
+
+ReadReceipt Room::lastReadReceipt(const QString& userId) const
+{
+ return d->lastReadReceipts.value(userId);
+}
+
+ReadReceipt Room::lastLocalReadReceipt() const
+{
+ return d->lastReadReceipts.value(localUser()->id());
+}
+
+Room::rev_iter_t Room::localReadReceiptMarker() const
+{
+ return findInTimeline(lastLocalReadReceipt().eventId);
}
-Room::rev_iter_t Room::readMarker() const
+QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; }
+
+Room::rev_iter_t Room::fullyReadMarker() const
{
return findInTimeline(d->fullyReadUntilEventId);
}
-QString Room::readMarkerEventId() const
+QSet<QString> Room::userIdsAtEvent(const QString& eventId)
{
- return d->fullyReadUntilEventId;
+ return d->eventIdReadUsers.value(eventId);
}
-QList<User*> Room::usersAtEventId(const QString& eventId)
+QSet<User*> Room::usersAtEventId(const QString& eventId)
{
- return d->eventIdReadUsers.values(eventId);
+ const auto& userIds = d->eventIdReadUsers.value(eventId);
+ QSet<User*> users;
+ users.reserve(userIds.size());
+ for (const auto& uId : userIds)
+ users.insert(user(uId));
+ return users;
}
-int Room::notificationCount() const { return d->notificationCount; }
+qsizetype Room::notificationCount() const
+{
+ return d->unreadStats.notableCount;
+}
void Room::resetNotificationCount()
{
- if (d->notificationCount == 0)
+ if (d->unreadStats.notableCount == 0)
return;
- d->notificationCount = 0;
+ d->unreadStats.notableCount = 0;
emit notificationCountChanged();
}
-int Room::highlightCount() const { return d->highlightCount; }
+qsizetype Room::highlightCount() const { return d->serverHighlightCount; }
void Room::resetHighlightCount()
{
- if (d->highlightCount == 0)
+ if (d->serverHighlightCount == 0)
return;
- d->highlightCount = 0;
+ d->serverHighlightCount = 0;
emit highlightCountChanged();
}
@@ -1141,8 +1331,8 @@ void Room::setTags(TagsMap newTags, ActionScope applyOn)
d->setTags(move(newTags));
connection()->callApi<SetAccountDataPerRoomJob>(
- localUser()->id(), id(), TagEvent::matrixTypeId(),
- TagEvent(d->tags).contentJson());
+ localUser()->id(), id(), TagEvent::TypeId,
+ Quotient::toJson(TagEvent::content_type { d->tags }));
if (propagate) {
for (auto* r = this; (r = r->successor(joinStates));)
@@ -1184,6 +1374,17 @@ QList<User*> Room::directChatUsers() const
return connection()->directChatUsers(this);
}
+QUrl Room::makeMediaUrl(const QString& eventId, const QUrl& mxcUrl) const
+{
+ auto url = connection()->makeMediaUrl(mxcUrl);
+ QUrlQuery q(url.query());
+ Q_ASSERT(q.hasQueryItem("user_id"));
+ q.addQueryItem("room_id", id());
+ q.addQueryItem("event_id", eventId);
+ url.setQuery(q);
+ return url;
+}
+
QString safeFileName(QString rawName)
{
return rawName.replace(QRegularExpression("[/\\<>|\"*?:]"), "_");
@@ -1237,9 +1438,8 @@ QUrl Room::urlToThumbnail(const QString& eventId) const
if (event->hasThumbnail()) {
auto* thumbnail = event->content()->thumbnailInfo();
Q_ASSERT(thumbnail != nullptr);
- return MediaThumbnailJob::makeRequestUrl(connection()->homeserver(),
- thumbnail->url,
- thumbnail->imageSize);
+ return connection()->getUrlForApi<MediaThumbnailJob>(
+ thumbnail->url(), thumbnail->imageSize);
}
qCDebug(MAIN) << "Event" << eventId << "has no thumbnail";
return {};
@@ -1250,8 +1450,7 @@ QUrl Room::urlToDownload(const QString& eventId) const
if (auto* event = d->getEventWithFile(eventId)) {
auto* fileInfo = event->content()->fileInfo();
Q_ASSERT(fileInfo != nullptr);
- return DownloadFileJob::makeRequestUrl(connection()->homeserver(),
- fileInfo->url);
+ return connection()->getUrlForApi<DownloadFileJob>(fileInfo->url());
}
return {};
}
@@ -1316,29 +1515,49 @@ QList<User*> Room::users() const { return d->membersMap.values(); }
QStringList Room::memberNames() const
{
+ return safeMemberNames();
+}
+
+QStringList Room::safeMemberNames() const
+{
QStringList res;
res.reserve(d->membersMap.size());
- for (auto u : qAsConst(d->membersMap))
- res.append(roomMembername(u));
+ for (const auto* u: std::as_const(d->membersMap))
+ res.append(safeMemberName(u->id()));
return res;
}
-int Room::memberCount() const { return d->membersMap.size(); }
+QStringList Room::htmlSafeMemberNames() const
+{
+ QStringList res;
+ res.reserve(d->membersMap.size());
+ for (const auto* u: std::as_const(d->membersMap))
+ res.append(htmlSafeMemberName(u->id()));
+
+ return res;
+}
int Room::timelineSize() const { return int(d->timeline.size()); }
bool Room::usesEncryption() const
{
- return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty();
+ return !currentState()
+ .queryOr(&EncryptionEvent::algorithm, QString())
+ .isEmpty();
}
-const StateEventBase* Room::getCurrentState(const QString& evtType,
- const QString& stateKey) const
+const StateEvent* Room::getCurrentState(const QString& evtType,
+ const QString& stateKey) const
{
return d->getCurrentState({ evtType, stateKey });
}
+RoomStateView Room::currentState() const
+{
+ return d->currentState;
+}
+
RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent)
{
#ifndef Quotient_E2EE_ENABLED
@@ -1346,39 +1565,64 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent)
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
return {};
#else // Quotient_E2EE_ENABLED
- if (encryptedEvent.algorithm() == MegolmV1AesSha2AlgoKey) {
- QString decrypted = d->groupSessionDecryptMessage(
- encryptedEvent.ciphertext(), encryptedEvent.senderKey(),
- encryptedEvent.sessionId(), encryptedEvent.id(),
- encryptedEvent.originTimestamp());
- if (decrypted.isEmpty()) {
- return {};
- }
- return makeEvent<RoomMessageEvent>(
- QJsonDocument::fromJson(decrypted.toUtf8()).object());
+ if (encryptedEvent.algorithm() != MegolmV1AesSha2AlgoKey) {
+ qWarning(E2EE) << "Algorithm of the encrypted event with id"
+ << encryptedEvent.id() << "is not decryptable by the current device";
+ return {};
+ }
+ QString decrypted = d->groupSessionDecryptMessage(
+ encryptedEvent.ciphertext(), encryptedEvent.sessionId(),
+ encryptedEvent.id(), encryptedEvent.originTimestamp(),
+ encryptedEvent.senderId());
+ if (decrypted.isEmpty()) {
+ // qCWarning(E2EE) << "Encrypted message is empty";
+ return {};
}
- qCDebug(E2EE) << "Algorithm of the encrypted event with id"
- << encryptedEvent.id() << "is not for the current device";
+ auto decryptedEvent = encryptedEvent.createDecrypted(decrypted);
+ if (decryptedEvent->roomId() == id()) {
+ return decryptedEvent;
+ }
+ qCWarning(E2EE) << "Decrypted event" << encryptedEvent.id() << "not for this room; discarding.";
return {};
#endif // Quotient_E2EE_ENABLED
}
void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent,
- const QString& senderKey)
+ const QString& senderId,
+ const QString& olmSessionId)
{
#ifndef Quotient_E2EE_ENABLED
Q_UNUSED(roomKeyEvent)
- Q_UNUSED(senderKey)
+ Q_UNUSED(senderId)
+ Q_UNUSED(olmSessionId)
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
#else // Quotient_E2EE_ENABLED
if (roomKeyEvent.algorithm() != MegolmV1AesSha2AlgoKey) {
qCWarning(E2EE) << "Ignoring unsupported algorithm"
<< roomKeyEvent.algorithm() << "in m.room_key event";
}
- if (d->addInboundGroupSession(senderKey, roomKeyEvent.sessionId(),
- roomKeyEvent.sessionKey())) {
- qCDebug(E2EE) << "added new inboundGroupSession:"
- << d->groupSessions.count();
+ if (d->addInboundGroupSession(roomKeyEvent.sessionId(),
+ roomKeyEvent.sessionKey(), senderId,
+ olmSessionId)) {
+ qCWarning(E2EE) << "added new inboundGroupSession:"
+ << d->groupSessions.size();
+ auto undecryptedEvents = d->undecryptedEvents[roomKeyEvent.sessionId()];
+ for (const auto& eventId : undecryptedEvents) {
+ const auto pIdx = d->eventsIndex.constFind(eventId);
+ if (pIdx == d->eventsIndex.cend())
+ continue;
+ auto& ti = d->timeline[Timeline::size_type(*pIdx - minTimelineIndex())];
+ if (auto encryptedEvent = ti.viewAs<EncryptedEvent>()) {
+ if (auto decrypted = decryptMessage(*encryptedEvent)) {
+ // The reference will survive the pointer being moved
+ auto& decryptedEvent = *decrypted;
+ auto oldEvent = ti.replaceEvent(std::move(decrypted));
+ decryptedEvent.setOriginalEvent(std::move(oldEvent));
+ emit replacedEvent(ti.event(), decryptedEvent.originalEvent());
+ d->undecryptedEvents[roomKeyEvent.sessionId()] -= eventId;
+ }
+ }
+ }
}
#endif // Quotient_E2EE_ENABLED
}
@@ -1402,29 +1646,35 @@ GetRoomEventsJob* Room::eventsHistoryJob() const { return d->eventsHistoryJob; }
Room::Changes Room::Private::setSummary(RoomSummary&& newSummary)
{
if (!summary.merge(newSummary))
- return Change::NoChange;
+ return Change::None;
qCDebug(STATE).nospace().noquote()
<< "Updated room summary for " << q->objectName() << ": " << summary;
- emit q->memberListChanged();
- return Change::SummaryChange;
+ return Change::Summary;
}
void Room::Private::insertMemberIntoMap(User* u)
{
- const auto userName =
- getCurrentState<RoomMemberEvent>(u->id())->displayName();
- // If there is exactly one namesake of the added user, signal member
- // renaming for that other one because the two should be disambiguated now.
+ const auto maybeUserName =
+ currentState.query(u->id(), &RoomMemberEvent::newDisplayName);
+ if (!maybeUserName)
+ qCWarning(MEMBERS) << "insertMemberIntoMap():" << u->id()
+ << "has no name (even empty)";
+ const auto userName = maybeUserName.value_or(QString());
const auto namesakes = membersMap.values(userName);
+ qCDebug(MEMBERS) << "insertMemberIntoMap(), user" << u->id()
+ << "with name" << userName << '-'
+ << namesakes.size() << "namesake(s) found";
- // Callers should check they are not adding an existing user once more.
+ // Callers should make sure they are not adding an existing user once more
Q_ASSERT(!namesakes.contains(u));
if (namesakes.contains(u)) { // Release version whines but continues
- qCCritical(STATE) << "Trying to add a user" << u->id() << "to room"
- << q->objectName() << "but that's already in it";
+ qCCritical(MEMBERS) << "Trying to add a user" << u->id() << "to room"
+ << q->objectName() << "but that's already in it";
return;
}
+ // If there is exactly one namesake of the added user, signal member
+ // renaming for that other one because the two should be disambiguated now
if (namesakes.size() == 1)
emit q->memberAboutToRename(namesakes.front(),
namesakes.front()->fullName(q));
@@ -1435,26 +1685,50 @@ void Room::Private::insertMemberIntoMap(User* u)
void Room::Private::removeMemberFromMap(User* u)
{
- const auto userName =
- getCurrentState<RoomMemberEvent>(u->id())->displayName();
+ const auto userName = currentState.queryOr(u->id(),
+ &RoomMemberEvent::newDisplayName,
+ QString());
+ qCDebug(MEMBERS) << "removeMemberFromMap(), username" << userName
+ << "for user" << u->id();
User* namesake = nullptr;
auto namesakes = membersMap.values(userName);
+ // If there was one namesake besides the removed user, signal member
+ // renaming for it because it doesn't need to be disambiguated any more.
if (namesakes.size() == 2) {
- namesake = namesakes.front() == u ? namesakes.back() : namesakes.front();
+ namesake =
+ namesakes.front() == u ? namesakes.back() : namesakes.front();
Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken");
emit q->memberAboutToRename(namesake, userName);
}
- membersMap.remove(userName, u);
- // If there was one namesake besides the removed user, signal member
- // renaming for it because it doesn't need to be disambiguated any more.
+ if (membersMap.remove(userName, u) == 0) {
+ qCDebug(MEMBERS) << "No entries removed; checking the whole list";
+ // Unless at the stage of initial filling, this no removed entries
+ // is suspicious; double-check that this user is not found in
+ // the whole map, and stop (for debug builds) or shout in the logs
+ // (for release builds) if there's one. That search is O(n), which
+ // may come rather expensive for larger rooms.
+ QElapsedTimer et;
+ auto it = std::find(membersMap.cbegin(), membersMap.cend(), u);
+ if (et.nsecsElapsed() > profilerMinNsecs() / 10)
+ qCDebug(MEMBERS) << "...done in" << et;
+ if (it != membersMap.cend()) {
+ // The assert (still) does more harm than good, it seems
+// Q_ASSERT_X(false, __FUNCTION__,
+// "Mismatched name in the room members list");
+ qCCritical(MEMBERS) << "Mismatched name in the room members list;"
+ " avoiding the list corruption";
+ membersMap.remove(it.key(), u);
+ }
+ }
if (namesake)
emit q->memberRenamed(namesake);
}
inline auto makeErrorStr(const Event& e, QByteArray msg)
{
- return msg.append("; event dump follows:\n").append(e.originalJson());
+ return msg.append("; event dump follows:\n")
+ .append(QJsonDocument(e.fullJson()).toJson());
}
Room::Timeline::size_type
@@ -1480,11 +1754,12 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events,
!eventsIndex.contains(eId), __FUNCTION__,
makeErrorStr(*e, "Event is already in the timeline; "
"incoming events were not properly deduplicated"));
- if (placement == Older)
- timeline.emplace_front(move(e), --index);
- else
- timeline.emplace_back(move(e), ++index);
+ const auto& ti = placement == Older
+ ? timeline.emplace_front(move(e), --index)
+ : timeline.emplace_back(move(e), ++index);
eventsIndex.insert(eId, index);
+ if (auto n = q->checkForNotifications(ti); n.type != Notification::None)
+ notifications.insert(e->id(), n);
Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId);
}
const auto insertedSize = (index - baseIndex) * placement;
@@ -1492,103 +1767,209 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events,
return Timeline::size_type(insertedSize);
}
+QString Room::memberName(const QString& mxId) const
+{
+ // See https://github.com/matrix-org/matrix-doc/issues/1375
+ if (const auto rme = currentState().get<RoomMemberEvent>(mxId)) {
+ if (rme->newDisplayName())
+ return *rme->newDisplayName();
+ if (rme->prevContent() && rme->prevContent()->displayName)
+ return *rme->prevContent()->displayName;
+ }
+ return {};
+}
+
QString Room::roomMembername(const User* u) const
{
+ Q_ASSERT(u != nullptr);
+ return disambiguatedMemberName(u->id());
+}
+
+QString Room::roomMembername(const QString& userId) const
+{
+ return disambiguatedMemberName(userId);
+}
+
+inline QString makeFullUserName(const QString& displayName, const QString& mxId)
+{
+ return displayName % " (" % mxId % ')';
+}
+
+QString Room::disambiguatedMemberName(const QString& mxId) const
+{
// See the CS spec, section 11.2.2.3
- const auto username = u->name(this);
+ const auto username = memberName(mxId);
if (username.isEmpty())
- return u->id();
+ return mxId;
auto namesakesIt = qAsConst(d->membersMap).find(username);
// We expect a user to be a member of the room - but technically it is
- // possible to invoke roomMemberName() even for non-members. In such case
+ // possible to invoke this function even for non-members. In such case
// we return the full name, just in case.
if (namesakesIt == d->membersMap.cend())
- return u->fullName(this);
+ return makeFullUserName(username, mxId);
auto nextUserIt = namesakesIt;
if (++nextUserIt == d->membersMap.cend() || nextUserIt.key() != username)
return username; // No disambiguation necessary
- return u->fullName(this); // Disambiguate fully
+ return makeFullUserName(username, mxId); // Disambiguate fully
}
-QString Room::roomMembername(const QString& userId) const
+QString Room::safeMemberName(const QString& userId) const
{
- if (auto* const u = user(userId))
- return roomMembername(u);
- return {};
+ return sanitized(disambiguatedMemberName(userId));
}
-QString Room::safeMemberName(const QString& userId) const
+QString Room::htmlSafeMemberName(const QString& userId) const
{
- return sanitized(roomMembername(userId));
+ return safeMemberName(userId).toHtmlEscaped();
+}
+
+QUrl Room::memberAvatarUrl(const QString &mxId) const
+{
+ // See https://github.com/matrix-org/matrix-doc/issues/1375
+ if (const auto rme = currentState().get<RoomMemberEvent>(mxId)) {
+ if (rme->newAvatarUrl())
+ return *rme->newAvatarUrl();
+ if (rme->prevContent() && rme->prevContent()->avatarUrl)
+ return *rme->prevContent()->avatarUrl;
+ }
+ return {};
+}
+
+Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data,
+ bool fromCache)
+{
+ Changes changes {};
+ if (fromCache) {
+ // Initial load of cached statistics
+ partiallyReadStats =
+ EventStats::fromCachedCounters(data.partiallyReadCount);
+ unreadStats = EventStats::fromCachedCounters(data.unreadCount,
+ data.highlightCount);
+ // Migrate from lib 0.6: -1 in the old unread counter overrides 0
+ // (which loads to an estimate) in notification_count. Next caching will
+ // save -1 in both places, completing the migration.
+ if (data.unreadCount == 0 && data.partiallyReadCount == -1)
+ unreadStats.isEstimate = false;
+ changes |= Change::PartiallyReadStats | Change::UnreadStats;
+ qCDebug(MESSAGES) << "Loaded" << q->objectName()
+ << "event statistics from cache:" << partiallyReadStats
+ << "since m.fully_read," << unreadStats
+ << "since m.read";
+ } else if (timeline.empty()) {
+ // In absence of actual events use statistics from the homeserver
+ if (merge(unreadStats.notableCount, data.unreadCount))
+ changes |= Change::PartiallyReadStats;
+ if (merge(unreadStats.highlightCount, data.highlightCount))
+ changes |= Change::UnreadStats;
+ unreadStats.isEstimate = !data.unreadCount.has_value()
+ || *data.unreadCount > 0;
+ qCDebug(MESSAGES)
+ << "Using server-side unread event statistics while the"
+ << q->objectName() << "timeline is empty:" << unreadStats;
+ }
+ bool correctedStats = false;
+ if (unreadStats.highlightCount > partiallyReadStats.highlightCount) {
+ correctedStats = true;
+ partiallyReadStats.highlightCount = unreadStats.highlightCount;
+ partiallyReadStats.isEstimate |= unreadStats.isEstimate;
+ }
+ if (unreadStats.notableCount > partiallyReadStats.notableCount) {
+ correctedStats = true;
+ partiallyReadStats.notableCount = unreadStats.notableCount;
+ partiallyReadStats.isEstimate |= unreadStats.isEstimate;
+ }
+ if (!unreadStats.isEstimate && partiallyReadStats.isEstimate) {
+ correctedStats = true;
+ partiallyReadStats.isEstimate = true;
+ }
+ if (correctedStats)
+ qCDebug(MESSAGES) << "Partially read event statistics in"
+ << q->objectName() << "were adjusted to"
+ << partiallyReadStats
+ << "to be consistent with the m.read receipt";
+ Q_ASSERT(partiallyReadStats.isValidFor(q, q->fullyReadMarker()));
+ Q_ASSERT(unreadStats.isValidFor(q, q->localReadReceiptMarker()));
+
+ // TODO: Once the library learns to count highlights, drop
+ // serverHighlightCount and only use the server-side counter when
+ // the timeline is empty (see the code above).
+ if (merge(serverHighlightCount, data.highlightCount)) {
+ qCDebug(MESSAGES) << "Updated highlights number in" << q->objectName()
+ << "to" << serverHighlightCount;
+ changes |= Change::Highlights;
+ }
+ return changes;
}
void Room::updateData(SyncRoomData&& data, bool fromCache)
{
+ qCDebug(MAIN) << "--- Updating room" << id() << "/" << objectName();
+ bool firstUpdate = d->baseState.empty();
+
if (d->prevBatch.isEmpty())
d->prevBatch = data.timelinePrevBatch;
setJoinState(data.joinState);
- Changes roomChanges = Change::NoChange;
+ Changes roomChanges {};
+ // The order of calculation is important - don't merge the lines!
+ roomChanges |= d->updateStateFrom(data.state);
+ roomChanges |= d->setSummary(move(data.summary));
+ roomChanges |= d->addNewMessageEvents(move(data.timeline));
+
+ for (auto&& ephemeralEvent : data.ephemeral)
+ roomChanges |= processEphemeralEvent(move(ephemeralEvent));
+
for (auto&& event : data.accountData)
roomChanges |= processAccountDataEvent(move(event));
- roomChanges |= d->updateStateFrom(data.state);
- // The order of calculation is important - don't merge these lines!
- roomChanges |= d->addNewMessageEvents(move(data.timeline));
+ roomChanges |= d->updateStatsFromSyncData(data, fromCache);
- if (roomChanges & TopicChange)
+ if (roomChanges & Change::Topic)
emit topicChanged();
- if (roomChanges & (NameChange | AliasesChange))
+ if (roomChanges & (Change::Name | Change::Aliases))
emit namesChanged(this);
- if (roomChanges & MembersChange)
- emit memberListChanged();
+ d->postprocessChanges(roomChanges, !fromCache);
+ if (firstUpdate)
+ emit baseStateLoaded();
+ qCDebug(MAIN) << "--- Finished updating room" << id() << "/" << objectName();
+}
- roomChanges |= d->setSummary(move(data.summary));
+void Room::Private::postprocessChanges(Changes changes, bool saveState)
+{
+ if (!changes)
+ return;
- for (auto&& ephemeralEvent : data.ephemeral)
- roomChanges |= processEphemeralEvent(move(ephemeralEvent));
+ if (changes & Change::Members)
+ emit q->memberListChanged();
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- // -2 is a special value to which SyncRoomData::SyncRoomData sets
- // unreadCount when it's missing in the payload (to distinguish from
- // explicit 0 in the payload).
- if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) {
- qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount;
- d->unreadMessages = data.unreadCount;
- emit unreadMessagesChanged(this);
- }
-
- // Similar to unreadCount, SyncRoomData constructor assigns -1 to
- // highlightCount/notificationCount when those are missing in the payload
- if (data.highlightCount != -1 && data.highlightCount != d->highlightCount) {
- qCDebug(MESSAGES).nospace()
- << "Highlights in " << objectName() //
- << ": " << d->highlightCount << " -> " << data.highlightCount;
- d->highlightCount = data.highlightCount;
- emit highlightCountChanged();
- }
- if (data.notificationCount != -1
- && data.notificationCount != d->notificationCount) //
- {
- qCDebug(MESSAGES).nospace()
- << "Notifications in " << objectName() //
- << ": " << d->notificationCount << " -> " << data.notificationCount;
- d->notificationCount = data.notificationCount;
- emit notificationCountChanged();
- }
- if (roomChanges != Change::NoChange) {
- d->updateDisplayname();
- emit changed(roomChanges);
- if (!fromCache)
- connection()->saveRoomState(this);
+ if (changes
+ & (Change::Name | Change::Aliases | Change::Members | Change::Summary))
+ updateDisplayname();
+
+ if (changes & Change::PartiallyReadStats) {
+ QT_IGNORE_DEPRECATIONS(
+ emit q->unreadMessagesChanged(q);) // TODO: remove in 0.8
+ emit q->partiallyReadStatsChanged();
}
+
+ if (changes & Change::UnreadStats)
+ emit q->unreadStatsChanged();
+
+ if (changes & Change::Highlights)
+ emit q->highlightCountChanged();
+
+ qCDebug(MAIN) << terse << changes << "= hex" << Qt::hex << uint(changes)
+ << "in" << q->objectName();
+ emit q->changed(changes);
+ if (saveState)
+ connection->saveRoomState(q);
}
RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event)
@@ -1608,41 +1989,73 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event)
QString Room::Private::sendEvent(RoomEventPtr&& event)
{
- if (q->usesEncryption()) {
- qCCritical(MAIN) << "Room" << q->objectName()
- << "enforces encryption; sending encrypted messages "
- "is not supported yet";
+ if (!q->successorId().isEmpty()) {
+ qCWarning(MAIN) << q << "has been upgraded, event won't be sent";
+ return {};
}
- if (q->successorId().isEmpty())
- return doSendEvent(addAsPending(std::move(event)));
- qCWarning(MAIN) << q << "has been upgraded, event won't be sent";
- return {};
+ return doSendEvent(addAsPending(std::move(event)));
}
QString Room::Private::doSendEvent(const RoomEvent* pEvent)
{
const auto txnId = pEvent->transactionId();
// TODO, #133: Enqueue the job rather than immediately trigger it.
+ const RoomEvent* _event = pEvent;
+ std::unique_ptr<EncryptedEvent> encryptedEvent;
+
+ if (q->usesEncryption()) {
+#ifndef Quotient_E2EE_ENABLED
+ qWarning() << "This build of libQuotient does not support E2EE.";
+ return {};
+#else
+ if (!hasValidMegolmSession() || shouldRotateMegolmSession()) {
+ createMegolmSession();
+ }
+ // Send the session to other people
+ connection->sendSessionKeyToDevices(
+ id, currentOutboundMegolmSession->sessionId(),
+ currentOutboundMegolmSession->sessionKey(), getDevicesWithoutKey(),
+ currentOutboundMegolmSession->sessionMessageIndex());
+
+ const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson());
+ currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1);
+ connection->saveCurrentOutboundMegolmSession(
+ id, *currentOutboundMegolmSession);
+ encryptedEvent = makeEvent<EncryptedEvent>(
+ encrypted, q->connection()->olmAccount()->identityKeys().curve25519,
+ q->connection()->deviceId(),
+ currentOutboundMegolmSession->sessionId());
+ encryptedEvent->setTransactionId(connection->generateTxnId());
+ encryptedEvent->setRoomId(id);
+ encryptedEvent->setSender(connection->userId());
+ if(pEvent->contentJson().contains("m.relates_to"_ls)) {
+ encryptedEvent->setRelation(pEvent->contentJson()["m.relates_to"_ls].toObject());
+ }
+ // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out
+ _event = encryptedEvent.get();
+#endif
+ }
+
if (auto call =
connection->callApi<SendMessageJob>(BackgroundRequest, id,
- pEvent->matrixType(), txnId,
- pEvent->contentJson())) {
+ _event->matrixType(), txnId,
+ _event->contentJson())) {
Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] {
auto it = q->findPendingEvent(txnId);
if (it == unsyncedEvents.end()) {
- qCWarning(EVENTS) << "Pending event for transaction" << txnId
+ qWarning(EVENTS) << "Pending event for transaction" << txnId
<< "not found - got synced so soon?";
return;
}
it->setDeparted();
- qCDebug(EVENTS) << "Event txn" << txnId << "has departed";
emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
});
- Room::connect(call, &BaseJob::failure, q,
- std::bind(&Room::Private::onEventSendingFailure, this,
- txnId, call));
- Room::connect(call, &BaseJob::success, q, [this, call, txnId] {
+ Room::connect(call, &BaseJob::result, q, [this, txnId, call] {
+ if (!call->status().good()) {
+ onEventSendingFailure(txnId, call);
+ return;
+ }
auto it = q->findPendingEvent(txnId);
if (it != unsyncedEvents.end()) {
if (it->deliveryStatus() != EventStatus::ReachedServer) {
@@ -1650,7 +2063,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
}
} else
- qCDebug(EVENTS) << "Pending event for transaction" << txnId
+ qDebug(EVENTS) << "Pending event for transaction" << txnId
<< "already merged";
emit q->messageSent(txnId, call->eventId());
@@ -1686,7 +2099,7 @@ QString Room::retryMessage(const QString& txnId)
<< "File for transaction" << txnId
<< "has already been uploaded, bypassing re-upload";
} else {
- if (isJobRunning(transferIt->job)) {
+ if (isJobPending(transferIt->job)) {
qCDebug(MESSAGES) << "Abandoning the upload job for transaction"
<< txnId << "and starting again";
transferIt->job->abandon();
@@ -1708,6 +2121,10 @@ QString Room::retryMessage(const QString& txnId)
return d->doSendEvent(it->event());
}
+// Using a function defers actual tr() invocation to the moment when
+// translations are initialised
+auto FileTransferCancelledMsg() { return Room::tr("File transfer cancelled"); }
+
void Room::discardMessage(const QString& txnId)
{
auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
@@ -1719,10 +2136,10 @@ void Room::discardMessage(const QString& txnId)
const auto& transferIt = d->fileTransfers.find(txnId);
if (transferIt != d->fileTransfers.end()) {
Q_ASSERT(transferIt->isUpload);
- if (isJobRunning(transferIt->job)) {
+ if (isJobPending(transferIt->job)) {
transferIt->status = FileTransferInfo::Cancelled;
transferIt->job->abandon();
- emit fileTransferFailed(txnId, tr("File upload cancelled"));
+ emit fileTransferFailed(txnId, FileTransferCancelledMsg());
} else if (transferIt->status == FileTransferInfo::Completed) {
qCWarning(MAIN)
<< "File for transaction" << txnId
@@ -1762,57 +2179,81 @@ QString Room::postReaction(const QString& eventId, const QString& key)
return d->sendEvent<ReactionEvent>(EventRelation::annotate(eventId, key));
}
-QString Room::postFile(const QString& plainText, const QUrl& localPath,
- bool asGenericFile)
+QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl)
{
- QFileInfo localFile { localPath.toLocalFile() };
- Q_ASSERT(localFile.isFile());
-
- const auto txnId =
- d->addAsPending(
- makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile))
- ->transactionId();
+ const auto txnId = addAsPending(move(msgEvent))->transactionId();
// Remote URL will only be known after upload; fill in the local path
// to enable the preview while the event is pending.
- uploadFile(txnId, localPath);
+ q->uploadFile(txnId, localUrl);
// Below, the upload job is used as a context object to clean up connections
- const auto& transferJob = d->fileTransfers.value(txnId).job;
- connect(this, &Room::fileTransferCompleted, transferJob,
- [this, txnId](const QString& id, const QUrl&, const QUrl& mxcUri) {
- if (id == txnId) {
- auto it = findPendingEvent(txnId);
- if (it != d->unsyncedEvents.end()) {
- it->setFileUploaded(mxcUri);
- emit pendingEventChanged(
- int(it - d->unsyncedEvents.begin()));
- d->doSendEvent(it->get());
- } else {
- // Normally in this situation we should instruct
- // the media server to delete the file; alas, there's no
- // API specced for that.
- qCWarning(MAIN) << "File uploaded to" << mxcUri
- << "but the event referring to it was "
- "cancelled";
- }
- }
- });
- connect(this, &Room::fileTransferCancelled, transferJob,
- [this, txnId](const QString& id) {
- if (id == txnId) {
- auto it = findPendingEvent(txnId);
- if (it != d->unsyncedEvents.end()) {
- const auto idx = int(it - d->unsyncedEvents.begin());
- emit pendingEventAboutToDiscard(idx);
- // See #286 on why iterator may not be valid here.
- d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx);
- emit pendingEventDiscarded();
- }
- }
+ const auto& transferJob = fileTransfers.value(txnId).job;
+ connect(q, &Room::fileTransferCompleted, transferJob,
+ [this, txnId](const QString& tId, const QUrl&,
+ const FileSourceInfo& fileMetadata) {
+ if (tId != txnId)
+ return;
+
+ const auto it = q->findPendingEvent(txnId);
+ if (it != unsyncedEvents.end()) {
+ it->setFileUploaded(fileMetadata);
+ emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
+ doSendEvent(it->get());
+ } else {
+ // Normally in this situation we should instruct
+ // the media server to delete the file; alas, there's no
+ // API specced for that.
+ qCWarning(MAIN)
+ << "File uploaded to" << getUrlFromSourceInfo(fileMetadata)
+ << "but the event referring to it was "
+ "cancelled";
+ }
+ });
+ connect(q, &Room::fileTransferFailed, transferJob,
+ [this, txnId](const QString& tId) {
+ if (tId != txnId)
+ return;
+
+ const auto it = q->findPendingEvent(txnId);
+ if (it == unsyncedEvents.end())
+ return;
+
+ const auto idx = int(it - unsyncedEvents.begin());
+ emit q->pendingEventAboutToDiscard(idx);
+ // See #286 on why `it` may not be valid here.
+ unsyncedEvents.erase(unsyncedEvents.begin() + idx);
+ emit q->pendingEventDiscarded();
});
return txnId;
}
+QString Room::postFile(const QString& plainText,
+ EventContent::TypedBase* content)
+{
+ Q_ASSERT(content != nullptr && content->fileInfo() != nullptr);
+ const auto* const fileInfo = content->fileInfo();
+ Q_ASSERT(fileInfo != nullptr);
+ QFileInfo localFile { fileInfo->url().toLocalFile() };
+ Q_ASSERT(localFile.isFile());
+
+ return d->doPostFile(
+ makeEvent<RoomMessageEvent>(
+ plainText, RoomMessageEvent::rawMsgTypeForFile(localFile), content),
+ fileInfo->url());
+}
+
+#if QT_VERSION_MAJOR < 6
+QString Room::postFile(const QString& plainText, const QUrl& localPath,
+ bool asGenericFile)
+{
+ QFileInfo localFile { localPath.toLocalFile() };
+ Q_ASSERT(localFile.isFile());
+ return d->doPostFile(makeEvent<RoomMessageEvent>(plainText, localFile,
+ asGenericFile),
+ localPath);
+}
+#endif
+
QString Room::postEvent(RoomEvent* event)
{
return d->sendEvent(RoomEventPtr(event));
@@ -1824,34 +2265,45 @@ QString Room::postJson(const QString& matrixType,
return d->sendEvent(loadEvent<RoomEvent>(matrixType, eventContent));
}
-SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) const
+SetRoomStateWithKeyJob* Room::setState(const StateEvent& evt)
+{
+ return setState(evt.matrixType(), evt.stateKey(), evt.contentJson());
+}
+
+SetRoomStateWithKeyJob* Room::setState(const QString& evtType,
+ const QString& stateKey,
+ const QJsonObject& contentJson)
{
- return d->requestSetState(evt);
+ return d->requestSetState(evtType, stateKey, contentJson);
}
void Room::setName(const QString& newName)
{
- d->requestSetState<RoomNameEvent>(newName);
+ setState<RoomNameEvent>(newName);
}
void Room::setCanonicalAlias(const QString& newAlias)
{
- d->requestSetState<RoomCanonicalAliasEvent>(newAlias, altAliases());
+ setState<RoomCanonicalAliasEvent>(newAlias, altAliases());
}
+void Room::setPinnedEvents(const QStringList& events)
+{
+ setState<RoomPinnedEvent>(events);
+}
void Room::setLocalAliases(const QStringList& aliases)
{
- d->requestSetState<RoomCanonicalAliasEvent>(canonicalAlias(), aliases);
+ setState<RoomCanonicalAliasEvent>(canonicalAlias(), aliases);
}
void Room::setTopic(const QString& newTopic)
{
- d->requestSetState<RoomTopicEvent>(newTopic);
+ setState<RoomTopicEvent>(newTopic);
}
bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re)
{
- if (le->type() != re->type())
+ if (le->metaType() != re->metaType())
return false;
if (!re->id().isEmpty())
@@ -1902,11 +2354,12 @@ void Room::sendCallCandidates(const QString& callId,
d->sendEvent<CallCandidatesEvent>(callId, candidates);
}
-void Room::answerCall(const QString& callId, const int lifetime,
+void Room::answerCall(const QString& callId, [[maybe_unused]] int lifetime,
const QString& sdp)
{
- Q_ASSERT(supportsCalls());
- d->sendEvent<CallAnswerEvent>(callId, lifetime, sdp);
+ qCWarning(MAIN) << "To client developer: drop lifetime parameter from "
+ "Room::answerCall(), it is no more accepted";
+ answerCall(callId, sdp);
}
void Room::answerCall(const QString& callId, const QString& sdp)
@@ -1921,17 +2374,20 @@ void Room::hangupCall(const QString& callId)
d->sendEvent<CallHangupEvent>(callId);
}
-void Room::getPreviousContent(int limit) { d->getPreviousContent(limit); }
+void Room::getPreviousContent(int limit, const QString& filter)
+{
+ d->getPreviousContent(limit, filter);
+}
-void Room::Private::getPreviousContent(int limit)
+void Room::Private::getPreviousContent(int limit, const QString &filter)
{
- if (isJobRunning(eventsHistoryJob))
+ if (isJobPending(eventsHistoryJob))
return;
- eventsHistoryJob =
- connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit);
+ eventsHistoryJob = connection->callApi<GetRoomEventsJob>(id, "b", prevBatch,
+ "", limit, filter);
emit q->eventsHistoryJobChanged();
- connect(eventsHistoryJob, &BaseJob::success, q, [=] {
+ connect(eventsHistoryJob, &BaseJob::success, q, [this] {
prevBatch = eventsHistoryJob->end();
addHistoricalMessageEvents(eventsHistoryJob->chunk());
});
@@ -1950,12 +2406,6 @@ LeaveRoomJob* Room::leaveRoom()
return connection()->leaveRoom(this);
}
-SetRoomStateWithKeyJob* Room::setMemberState(const QString& memberId,
- const RoomMemberEvent& event) const
-{
- return d->requestSetState<RoomMemberEvent>(memberId, event.content());
-}
-
void Room::kickMember(const QString& memberId, const QString& reason)
{
connection()->callApi<KickJob>(id(), memberId, reason);
@@ -1983,18 +2433,35 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename,
Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__,
"localFilename should point at a local file");
auto fileName = localFilename.toLocalFile();
+ FileSourceInfo fileMetadata;
+#ifdef Quotient_E2EE_ENABLED
+ QTemporaryFile tempFile;
+ if (usesEncryption()) {
+ tempFile.open();
+ QFile file(localFilename.toLocalFile());
+ file.open(QFile::ReadOnly);
+ QByteArray data;
+ std::tie(fileMetadata, data) = encryptFile(file.readAll());
+ tempFile.write(data);
+ tempFile.close();
+ fileName = QFileInfo(tempFile).absoluteFilePath();
+ }
+#endif
auto job = connection()->uploadFile(fileName, overrideContentType);
- if (isJobRunning(job)) {
+ if (isJobPending(job)) {
d->fileTransfers[id] = { job, fileName, true };
connect(job, &BaseJob::uploadProgress, this,
[this, id](qint64 sent, qint64 total) {
d->fileTransfers[id].update(sent, total);
emit fileTransferProgress(id, sent, total);
});
- connect(job, &BaseJob::success, this, [this, id, localFilename, job] {
- d->fileTransfers[id].status = FileTransferInfo::Completed;
- emit fileTransferCompleted(id, localFilename, job->contentUri());
- });
+ connect(job, &BaseJob::success, this,
+ [this, id, localFilename, job, fileMetadata]() mutable {
+ // The lambda is mutable to change encryptedFileMetadata
+ d->fileTransfers[id].status = FileTransferInfo::Completed;
+ setUrlInSourceInfo(fileMetadata, QUrl(job->contentUri()));
+ emit fileTransferCompleted(id, localFilename, fileMetadata);
+ });
connect(job, &BaseJob::failure, this,
std::bind(&Private::failedTransfer, d, id, job->errorString()));
emit newFileTransfer(id, localFilename);
@@ -2027,11 +2494,11 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
<< "has an empty or malformed mxc URL; won't download";
return;
}
- const auto fileUrl = fileInfo->url;
+ const auto fileUrl = fileInfo->url();
auto filePath = localFilename.toLocalFile();
if (filePath.isEmpty()) { // Setup default file path
filePath =
- fileInfo->url.path().mid(1) % '_' % d->fileNameToDownload(event);
+ fileInfo->url().path().mid(1) % '_' % d->fileNameToDownload(event);
if (filePath.size() > 200) // If too long, elide in the middle
filePath.replace(128, filePath.size() - 192, "---");
@@ -2039,8 +2506,18 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
filePath = QDir::tempPath() % '/' % filePath;
qDebug(MAIN) << "File path:" << filePath;
}
- auto job = connection()->downloadFile(fileUrl, filePath);
- if (isJobRunning(job)) {
+ DownloadFileJob *job = nullptr;
+#ifdef Quotient_E2EE_ENABLED
+ if (auto* fileMetadata =
+ std::get_if<EncryptedFileMetadata>(&fileInfo->source)) {
+ job = connection()->downloadFile(fileUrl, *fileMetadata, filePath);
+ } else {
+#endif
+ job = connection()->downloadFile(fileUrl, filePath);
+#ifdef Quotient_E2EE_ENABLED
+ }
+#endif
+ if (isJobPending(job)) {
// If there was a previous transfer (completed or failed), overwrite it.
d->fileTransfers[eventId] = { job, job->targetFileName() };
connect(job, &BaseJob::downloadProgress, this,
@@ -2056,22 +2533,23 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
connect(job, &BaseJob::failure, this,
std::bind(&Private::failedTransfer, d, eventId,
job->errorString()));
+ emit newFileTransfer(eventId, localFilename);
} else
d->failedTransfer(eventId);
}
void Room::cancelFileTransfer(const QString& id)
{
- const auto it = d->fileTransfers.constFind(id);
- if (it == d->fileTransfers.cend()) {
+ const auto it = d->fileTransfers.find(id);
+ if (it == d->fileTransfers.end()) {
qCWarning(MAIN) << "No information on file transfer" << id << "in room"
<< d->id;
return;
}
- if (isJobRunning(it->job))
+ if (isJobPending(it->job))
it->job->abandon();
- d->fileTransfers.remove(id);
- emit fileTransferCancelled(id);
+ it->status = FileTransferInfo::Cancelled;
+ emit fileTransferFailed(id, FileTransferCancelledMsg());
}
void Room::Private::dropDuplicateEvents(RoomEvents& events) const
@@ -2099,6 +2577,26 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const
events.erase(dupsBegin, events.end());
}
+void Room::Private::decryptIncomingEvents(RoomEvents& events)
+{
+#ifdef Quotient_E2EE_ENABLED
+ QElapsedTimer et;
+ et.start();
+ size_t totalDecrypted = 0;
+ for (auto& eptr : events)
+ if (const auto& eeptr = eventCast<EncryptedEvent>(eptr)) {
+ if (auto decrypted = q->decryptMessage(*eeptr)) {
+ ++totalDecrypted;
+ auto&& oldEvent = exchange(eptr, move(decrypted));
+ eptr->setOriginalEvent(::move(oldEvent));
+ } else
+ undecryptedEvents[eeptr->sessionId()] += eeptr->id();
+ }
+ if (totalDecrypted > 5 || et.nsecsElapsed() >= profilerMinNsecs())
+ qDebug(PROFILER) << "Decrypted" << totalDecrypted << "events in" << et;
+#endif
+}
+
/** Make a redacted event
*
* This applies the redaction procedure as defined by the CS API specification
@@ -2108,10 +2606,10 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const
RoomEventPtr makeRedacted(const RoomEvent& target,
const RedactionEvent& redaction)
{
- auto originalJson = target.originalJsonObject();
+ auto originalJson = target.fullJson();
// clang-format off
- static const QStringList keepKeys { EventIdKey, TypeKey,
- QStringLiteral("room_id"), QStringLiteral("sender"), StateKeyKey,
+ static const QStringList keepKeys {
+ EventIdKey, TypeKey, RoomIdKey, SenderKey, StateKeyKey,
QStringLiteral("hashes"), QStringLiteral("signatures"),
QStringLiteral("depth"), QStringLiteral("prev_events"),
QStringLiteral("prev_state"), QStringLiteral("auth_events"),
@@ -2119,18 +2617,18 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
QStringLiteral("membership") };
// clang-format on
- std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap {
- { RoomMemberEvent::typeId(), { QStringLiteral("membership") } },
- { RoomCreateEvent::typeId(), { QStringLiteral("creator") } },
- { RoomPowerLevelsEvent::typeId(),
+ static const std::pair<event_type_t, QStringList> keepContentKeysMap[]{
+ { RoomMemberEvent::TypeId, { QStringLiteral("membership") } },
+ { RoomCreateEvent::TypeId, { QStringLiteral("creator") } },
+ { RoomPowerLevelsEvent::TypeId,
{ QStringLiteral("ban"), QStringLiteral("events"),
QStringLiteral("events_default"), QStringLiteral("kick"),
QStringLiteral("redact"), QStringLiteral("state_default"),
QStringLiteral("users"), QStringLiteral("users_default") } },
- { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } }
- // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } }
- // , { RoomHistoryVisibility::typeId(),
- // { QStringLiteral("history_visibility") } }
+ // TODO: Replace with RoomJoinRules::TypeId etc. once available
+ { "m.room.join_rules"_ls, { QStringLiteral("join_rule") } },
+ { "m.room.history_visibility"_ls,
+ { QStringLiteral("history_visibility") } }
};
for (auto it = originalJson.begin(); it != originalJson.end();) {
if (!keepKeys.contains(it.key()))
@@ -2139,9 +2637,9 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
++it;
}
auto keepContentKeys =
- find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(),
+ find_if(begin(keepContentKeysMap), end(keepContentKeysMap),
[&target](const auto& t) { return target.type() == t.first; });
- if (keepContentKeys == keepContentKeysMap.end()) {
+ if (keepContentKeys == end(keepContentKeysMap)) {
originalJson.remove(ContentKeyL);
originalJson.remove(PrevContentKeyL);
} else {
@@ -2155,7 +2653,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
originalJson.insert(ContentKey, content);
}
auto unsignedData = originalJson.take(UnsignedKeyL).toObject();
- unsignedData[RedactedCauseKeyL] = redaction.originalJsonObject();
+ unsignedData[RedactedCauseKeyL] = redaction.fullJson();
originalJson.insert(QStringLiteral("unsigned"), unsignedData);
return loadEvent<RoomEvent>(originalJson);
@@ -2183,12 +2681,14 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction));
qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id();
if (oldEvent->isStateEvent()) {
- const StateEventKey evtKey { oldEvent->matrixType(),
- oldEvent->stateKey() };
- Q_ASSERT(currentState.contains(evtKey));
- if (currentState.value(evtKey) == oldEvent.get()) {
- Q_ASSERT(ti.index() >= 0); // Historical states can't be in
- // currentState
+ // Check whether the old event was a part of current state; if it was,
+ // update the current state to the redacted event object.
+ const auto currentStateEvt =
+ currentState.get(oldEvent->matrixType(), oldEvent->stateKey());
+ Q_ASSERT(currentStateEvt);
+ if (currentStateEvt == oldEvent.get()) {
+ // Historical states can't be in currentState
+ Q_ASSERT(ti.index() >= 0);
qCDebug(STATE).nospace()
<< "Redacting state " << oldEvent->matrixType() << "/"
<< oldEvent->stateKey();
@@ -2200,8 +2700,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
}
if (const auto* reaction = eventCast<ReactionEvent>(oldEvent)) {
const auto& targetEvtId = reaction->relation().eventId;
- const auto lookupKey =
- qMakePair(targetEvtId, EventRelation::Annotation());
+ const std::pair lookupKey { targetEvtId, EventRelation::AnnotationType };
if (relations.contains(lookupKey)) {
relations[lookupKey].removeOne(reaction);
emit q->updatedEvent(targetEvtId);
@@ -2209,6 +2708,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
}
q->onRedaction(*oldEvent, *ti);
emit q->replacedEvent(ti.event(), rawPtr(oldEvent));
+ // By now, all references to oldEvent must have been updated to ti.event()
return true;
}
@@ -2220,8 +2720,13 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
RoomEventPtr makeReplaced(const RoomEvent& target,
const RoomMessageEvent& replacement)
{
- auto originalJson = target.originalJsonObject();
- originalJson[ContentKeyL] = replacement.contentJson().value("m.new_content"_ls);
+ const auto& targetReply = target.contentPart<QJsonObject>("m.relates_to");
+ auto newContent = replacement.contentPart<QJsonObject>("m.new_content"_ls);
+ if (!targetReply.empty()) {
+ newContent["m.relates_to"] = targetReply;
+ }
+ auto originalJson = target.fullJson();
+ originalJson[ContentKeyL] = newContent;
auto unsignedData = originalJson.take(UnsignedKeyL).toObject();
auto relations = unsignedData.take("m.relations"_ls).toObject();
@@ -2281,10 +2786,13 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
{
dropDuplicateEvents(events);
if (events.empty())
- return Change::NoChange;
+ return Change::None;
+
+ decryptIncomingEvents(events);
QElapsedTimer et;
et.start();
+
{
// Pre-process redactions and edits so that events that get
// redacted/replaced in the same batch landed in the timeline already
@@ -2334,7 +2842,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
// clients historically expect. This may eventually change though if we
// postulate that the current state is only current between syncs but not
// within a sync.
- Changes roomChanges = Change::NoChange;
+ Changes roomChanges {};
for (const auto& eptr : events)
roomChanges |= q->processStateEvent(*eptr);
@@ -2391,7 +2899,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
if (q->supportsCalls())
for (auto it = from; it != syncEdge(); ++it)
- if (const auto* evt = it->viewAs<CallEventBase>())
+ if (const auto* evt = it->viewAs<CallEvent>())
emit q->callEvent(q, evt);
if (totalInserted > 0) {
@@ -2407,23 +2915,16 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
<< totalInserted << "new events; the last event is now"
<< timeline.back();
- // The first event in the just-added batch (referred to by `from`)
- // defines whose read receipt can possibly be promoted any further over
- // the same author's events newly arrived. Others will need explicit
- // read receipts from the server - or, for the local user, calling
- // setLastDisplayedEventId() - to promote their read receipts over
- // the new message events.
- if (auto* const firstWriter = q->user((*from)->senderId())) {
- setLastReadReceipt(firstWriter, rev_iter_t(from + 1));
- if (firstWriter == q->localUser() && q->readMarker().base() == from) {
- // If the local user's message(s) is/are first in the batch
- // and the fully read marker was right before it, promote
- // the fully read marker to the same event as the read receipt.
- roomChanges |=
- setFullyReadMarker(lastReadEventIds.value(firstWriter));
- }
- }
- roomChanges |= updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
+ roomChanges |= updateStats(timeline.crbegin(), rev_iter_t(from));
+
+ // If the local user's message(s) is/are first in the batch
+ // and the fully read marker was right before it, promote
+ // the fully read marker to the same event as the read receipt.
+ const auto& firstWriterId = (*from)->senderId();
+ if (firstWriterId == connection->userId()
+ && q->fullyReadMarker().base() == from)
+ roomChanges |=
+ setFullyReadMarker(q->lastReadReceipt(firstWriterId).eventId);
}
Q_ASSERT(timeline.size() == timelineSize + totalInserted);
@@ -2435,14 +2936,17 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
{
- QElapsedTimer et;
- et.start();
const auto timelineSize = timeline.size();
dropDuplicateEvents(events);
if (events.empty())
return;
+ decryptIncomingEvents(events);
+
+ QElapsedTimer et;
+ et.start();
+ Changes changes {};
// In case of lazy-loading new members may be loaded with historical
// messages. Also, the cache doesn't store events with empty content;
// so when such events show up in the timeline they should be properly
@@ -2450,8 +2954,8 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
for (const auto& eptr : events) {
const auto& e = *eptr;
if (e.isStateEvent()
- && !currentState.contains({ e.matrixType(), e.stateKey() })) {
- q->processStateEvent(e);
+ && !currentState.contains(e.matrixType(), e.stateKey())) {
+ changes |= q->processStateEvent(e);
}
}
@@ -2471,108 +2975,133 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
emit q->updatedEvent(relation.eventId);
}
}
- if (updateUnreadCount(from, historyEdge()) != NoChange)
- connection->saveRoomState(q);
-
- // When there are no unread messages and the read marker is within the
- // known timeline, unreadMessages == -1
- // (see https://github.com/quotient-im/libQuotient/wiki/unread_count).
- Q_ASSERT(unreadMessages != 0 || q->readMarker() == historyEdge());
-
Q_ASSERT(timeline.size() == timelineSize + insertedSize);
if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs())
qCDebug(PROFILER) << "Added" << insertedSize << "historical event(s) to"
<< q->objectName() << "in" << et;
+
+ changes |= updateStats(from, historyEdge());
+ if (changes)
+ postprocessChanges(changes);
}
Room::Changes Room::processStateEvent(const RoomEvent& e)
{
if (!e.isStateEvent())
- return Change::NoChange;
-
- auto* const sender = user(e.senderId());
- if (!sender) {
- qCWarning(MAIN) << "State event" << e.id()
- << "is invalid and won't be processed";
- return Change::NoChange;
- }
+ return Change::None;
// Find a value (create an empty one if necessary) and get a reference
- // to it. Can't use getCurrentState<>() because it (creates and) returns
- // a stub if a value is not found, and what's needed here is a "real" event
- // or nullptr.
+ // to it, anticipating a change further in the function.
auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }];
// Prepare for the state change
- const auto oldRme = static_cast<const RoomMemberEvent*>(curStateEvent);
- visit(e, [this, &oldRme](const RoomMemberEvent& rme) {
- auto* const u = user(rme.userId());
- if (!u) { // Invalid user id?
- qCWarning(MAIN)
- << "Could not get a user object for" << rme.userId();
- return;
- }
- // TODO: remove along with User::processEvent() in 0.7
- const auto prevMembership = oldRme ? oldRme->membership()
- : MembershipType::Leave;
- u->processEvent(rme, this, oldRme == nullptr);
-
- switch (prevMembership) {
- case MembershipType::Invite:
- if (rme.membership() != prevMembership) {
- d->usersInvited.removeOne(u);
- Q_ASSERT(!d->usersInvited.contains(u));
+ // clang-format off
+ const bool proceed = switchOnType(e
+ , [this, curStateEvent](const RoomMemberEvent& rme) {
+ // clang-format on
+ auto* oldRme = static_cast<const RoomMemberEvent*>(curStateEvent);
+ auto* u = user(rme.userId());
+ if (!u) { // Some terribly malformed user id?
+ qCCritical(MAIN) << "Could not get a user object for"
+ << rme.userId();
+ return false; // Stay low and hope for the best...
}
- break;
- case MembershipType::Join:
- switch (rme.membership()) {
- case MembershipType::Join: // rename/avatar change or no-op
- if (rme.displayName() != oldRme->displayName()) {
- emit memberAboutToRename(u, rme.displayName());
+ const auto prevMembership = oldRme ? oldRme->membership()
+ : Membership::Leave;
+ switch (prevMembership) {
+ case Membership::Invite:
+ if (rme.membership() != prevMembership) {
+ d->usersInvited.removeOne(u);
+ Q_ASSERT(!d->usersInvited.contains(u));
+ }
+ break;
+ case Membership::Join:
+ if (rme.membership() == Membership::Join) {
+ // rename/avatar change or no-op
+ if (rme.newDisplayName()) {
+ emit memberAboutToRename(u, *rme.newDisplayName());
+ d->removeMemberFromMap(u);
+ }
+ if (!rme.newDisplayName() && !rme.newAvatarUrl()) {
+ qCWarning(MEMBERS)
+ << "No-op membership event for" << rme.userId()
+ << "- retaining the state";
+ qCWarning(MEMBERS) << "The event dump:" << rme;
+ return false;
+ }
+ } else {
+ if (rme.membership() == Membership::Invite)
+ qCWarning(MAIN)
+ << "Membership change from Join to Invite:" << rme;
+ // whatever the new membership, it's no more Join
d->removeMemberFromMap(u);
+ emit userRemoved(u);
}
break;
- case MembershipType::Invite:
- qCWarning(MAIN) << "Membership change from Join to Invite:"
- << rme;
- [[fallthrough]];
- default: // whatever the new membership, it's no more Join
- d->removeMemberFromMap(u);
- emit userRemoved(u);
+ case Membership::Ban:
+ case Membership::Knock:
+ case Membership::Leave:
+ if (rme.membership() == Membership::Invite
+ || rme.membership() == Membership::Join) {
+ d->membersLeft.removeOne(u);
+ Q_ASSERT(!d->membersLeft.contains(u));
+ }
+ break;
+ case Membership::Undefined:
+ ; // A warning will be dropped in the post-processing block below
}
- break;
- default:
- if (rme.membership() == MembershipType::Invite
- || rme.membership() == MembershipType::Join) {
- d->membersLeft.removeOne(u);
- Q_ASSERT(!d->membersLeft.contains(u));
+ return true;
+ // clang-format off
+ }
+ , [this, curStateEvent]( const EncryptionEvent& ee) {
+ // clang-format on
+ auto* oldEncEvt =
+ static_cast<const EncryptionEvent*>(curStateEvent);
+ if (ee.algorithm().isEmpty()) {
+ qWarning(STATE)
+ << "The encryption event for room" << objectName()
+ << "doesn't have 'algorithm' specified - ignoring";
+ return false;
}
+ if (oldEncEvt
+ && oldEncEvt->encryption() != EncryptionType::Undefined) {
+ qCWarning(STATE) << "The room is already encrypted but a new"
+ " room encryption event arrived - ignoring";
+ return false;
+ }
+ return true;
+ // clang-format off
}
- });
+ , true); // By default, go forward with the state change
+ // clang-format on
+ if (!proceed) {
+ if (!curStateEvent) // Remove the empty placeholder if one was created
+ d->currentState.remove({ e.matrixType(), e.stateKey() });
+ return Change::None;
+ }
// Change the state
const auto* const oldStateEvent =
- std::exchange(curStateEvent, static_cast<const StateEventBase*>(&e));
+ std::exchange(curStateEvent, static_cast<const StateEvent*>(&e));
Q_ASSERT(!oldStateEvent
|| (oldStateEvent->matrixType() == e.matrixType()
&& oldStateEvent->stateKey() == e.stateKey()));
- if (!is<RoomMemberEvent>(e)) // Room member events are too numerous
+ if (is<RoomMemberEvent>(e))
+ qCDebug(MEMBERS) << "Updated room member state:" << e;
+ else
qCDebug(STATE) << "Updated room state:" << e;
// Update internal structures as per the change and work out the return value
// clang-format off
- return visit(e
+ const auto result = switchOnType(e
, [] (const RoomNameEvent&) {
- return NameChange;
- }
- , [] (const RoomAliasesEvent&) {
- return NoChange; // This event has been removed by MSC2432
+ return Change::Name;
}
, [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) {
// clang-format on
setObjectName(cae.alias().isEmpty() ? d->id : cae.alias());
const auto* oldCae =
- static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent);
+ static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent);
QStringList previousAltAliases {};
if (oldCae) {
previousAltAliases = oldCae->altAliases();
@@ -2584,73 +3113,68 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
if (!cae.alias().isEmpty())
newAliases.push_front(cae.alias());
- connection()->updateRoomAliases(id(), previousAltAliases, newAliases);
- return AliasesChange;
+ connection()->updateRoomAliases(id(), previousAltAliases,
+ newAliases);
+ return Change::Aliases;
// clang-format off
}
+ , [this] (const RoomPinnedEvent&) {
+ emit pinnedEventsChanged();
+ return Change::Other;
+ }
, [] (const RoomTopicEvent&) {
- return TopicChange;
+ return Change::Topic;
}
, [this] (const RoomAvatarEvent& evt) {
if (d->avatar.updateUrl(evt.url()))
emit avatarChanged();
- return AvatarChange;
+ return Change::Avatar;
}
- , [this,oldRme,sender] (const RoomMemberEvent& evt) {
+ , [this,oldStateEvent] (const RoomMemberEvent& evt) {
// clang-format on
auto* u = user(evt.userId());
- if (!u)
- return NoChange; // Already warned earlier
- // TODO: remove in 0.7
- u->processEvent(evt, this, oldRme == nullptr);
-
- const auto prevMembership = oldRme ? oldRme->membership()
- : MembershipType::Leave;
+ const auto* oldMemberEvent =
+ static_cast<const RoomMemberEvent*>(oldStateEvent);
+ const auto prevMembership = oldMemberEvent
+ ? oldMemberEvent->membership()
+ : Membership::Leave;
switch (evt.membership()) {
- case MembershipType::Join:
- if (prevMembership != MembershipType::Join) {
+ case Membership::Join:
+ if (prevMembership != Membership::Join) {
d->insertMemberIntoMap(u);
emit userAdded(u);
- } else if (oldRme->displayName() != evt.displayName()) {
- d->insertMemberIntoMap(u);
- emit memberRenamed(u);
+ } else {
+ if (evt.newDisplayName()) {
+ d->insertMemberIntoMap(u);
+ emit memberRenamed(u);
+ }
+ if (evt.newAvatarUrl())
+ emit memberAvatarChanged(u);
}
break;
- case MembershipType::Invite:
+ case Membership::Invite:
if (!d->usersInvited.contains(u))
d->usersInvited.push_back(u);
if (u == localUser() && evt.isDirect())
- connection()->addToDirectChats(this, sender);
+ connection()->addToDirectChats(this, user(evt.senderId()));
break;
- case MembershipType::Knock:
- case MembershipType::Ban:
- case MembershipType::Leave:
+ case Membership::Knock:
+ case Membership::Ban:
+ case Membership::Leave:
if (!d->membersLeft.contains(u))
d->membersLeft.append(u);
+ break;
+ case Membership::Undefined:
+ qCWarning(MEMBERS) << "Ignored undefined membership type";
}
- return MembersChange;
+ return Change::Members;
// clang-format off
}
- , [this, oldEncEvt = static_cast<const EncryptionEvent*>(oldStateEvent)](
- const EncryptionEvent& ee) {
- // clang-format on
- if (ee.algorithm().isEmpty()) {
- qWarning(STATE)
- << "The encryption event for room" << objectName()
- << "doesn't have 'algorithm' specified - ignoring";
- return NoChange;
- }
- if (oldEncEvt
- && oldEncEvt->encryption() != EncryptionEventContent::Undefined) {
- qCWarning(STATE) << "The room is already encrypted but a new"
- " room encryption event arrived - ignoring";
- return NoChange;
- }
+ , [this] (const EncryptionEvent&) {
// As encryption can only be switched on once, emit the signal here
// instead of aggregating and emitting in updateData()
emit encryption();
- return OtherChange;
- // clang-format off
+ return Change::Other;
}
, [this] (const RoomTombstoneEvent& evt) {
const auto successorId = evt.successorRoomId();
@@ -2666,80 +3190,93 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return true;
});
- return OtherChange;
+ return Change::Other;
+ // clang-format off
}
- );
+ , Change::Other);
// clang-format on
+ Q_ASSERT(result != Change::None);
+ return result;
}
Room::Changes Room::processEphemeralEvent(EventPtr&& event)
{
- Changes changes = NoChange;
+ Changes changes {};
QElapsedTimer et;
et.start();
- if (auto* evt = eventCast<TypingEvent>(event)) {
- d->usersTyping.clear();
- for (const QString& userId : qAsConst(evt->users())) {
- auto* const u = user(userId);
- if (memberJoinState(u) == JoinState::Join)
- d->usersTyping.append(u);
- }
- if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER)
- << "Processing typing events from" << evt->users().size()
- << "user(s) in" << objectName() << "took" << et;
- emit typingChanged();
- }
- if (auto* evt = eventCast<ReceiptEvent>(event)) {
- int totalReceipts = 0;
- for (const auto& p : qAsConst(evt->eventsWithReceipts())) {
- totalReceipts += p.receipts.size();
- {
- if (p.receipts.size() == 1)
- qCDebug(EPHEMERAL)
- << objectName() << "received a read receipt for"
- << p.evtId << "from" << p.receipts[0].userId;
- else
- qCDebug(EPHEMERAL)
- << objectName() << "received read receipts for"
- << p.evtId << "from" << p.receipts.size() << "users";
- }
- const auto newMarker = findInTimeline(p.evtId);
- if (newMarker == historyEdge())
- qCDebug(EPHEMERAL) << "Event of the read receipt(s) is not "
- "found; saving them anyway";
- for (const Receipt& r : p.receipts)
- if (auto* const u = user(r.userId);
- memberJoinState(u) == JoinState::Join) {
- // If the event is not found (most likely, because it's
- // too old and hasn't been fetched from the server yet)
- // but there is a previous marker for a user, keep
- // the previous marker because read receipts are not
- // supposed to move backwards. Otherwise, blindly
- // store the event id for this user and update the read
- // marker when/if the event is fetched later on.
- d->setLastReadReceipt(u, newMarker, p.evtId);
+ switchOnType(*event,
+ [this, &et](const TypingEvent& evt) {
+ const auto& users = evt.users();
+ d->usersTyping.clear();
+ d->usersTyping.reserve(users.size()); // Assume all are members
+ for (const auto& userId : users)
+ if (isMember(userId))
+ d->usersTyping.append(user(userId));
+
+ if (d->usersTyping.size() > 3
+ || et.nsecsElapsed() >= profilerMinNsecs())
+ qDebug(PROFILER)
+ << "Processing typing events from" << users.size()
+ << "user(s) in" << objectName() << "took" << et;
+ emit typingChanged();
+ },
+ [this, &changes, &et](const ReceiptEvent& evt) {
+ const auto& receiptsJson = evt.contentJson();
+ QVector<QString> updatedUserIds;
+ // Most often (especially for bigger batches), receipts are
+ // scattered across events (an anecdotal evidence showed 1.2-1.3
+ // receipts per event on average).
+ updatedUserIds.reserve(receiptsJson.size() * 2);
+ for (auto eventIt = receiptsJson.begin();
+ eventIt != receiptsJson.end(); ++eventIt) {
+ const auto evtId = eventIt.key();
+ const auto newMarker = findInTimeline(evtId);
+ if (newMarker == historyEdge())
+ qDebug(EPHEMERAL)
+ << "Event" << evtId
+ << "is not found; saving read receipt(s) anyway";
+ const auto reads =
+ eventIt.value().toObject().value("m.read"_ls).toObject();
+ for (auto userIt = reads.begin(); userIt != reads.end();
+ ++userIt) {
+ ReadReceipt rr{ evtId,
+ fromJson<QDateTime>(
+ userIt->toObject().value("ts"_ls)) };
+ const auto userId = userIt.key();
+ if (userId == connection()->userId()) {
+ // Local user is special, and will get a signal about
+ // its read receipt separately from (and before) a
+ // signal on everybody else. No particular reason, just
+ // less cumbersome code.
+ changes |= d->setLocalLastReadReceipt(newMarker, rr);
+ } else if (d->setLastReadReceipt(userId, newMarker, rr)) {
+ changes |= Change::Other;
+ updatedUserIds.push_back(userId);
+ }
}
- }
- if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10
- || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "Processing" << totalReceipts << "receipt(s) on"
- << evt->eventsWithReceipts().size()
- << "event(s) in" << objectName() << "took" << et;
- }
+ }
+ if (updatedUserIds.size() > 10
+ || et.nsecsElapsed() >= profilerMinNsecs())
+ qDebug(PROFILER)
+ << "Processing" << updatedUserIds.size()
+ << "non-local receipt(s) on" << receiptsJson.size()
+ << "event(s) in" << objectName() << "took" << et;
+ if (!updatedUserIds.empty())
+ emit lastReadEventChanged(updatedUserIds);
+ });
return changes;
}
Room::Changes Room::processAccountDataEvent(EventPtr&& event)
{
- Changes changes = NoChange;
+ Changes changes {};
if (auto* evt = eventCast<TagEvent>(event)) {
d->setTags(evt->tags());
- changes |= Change::TagsChange;
+ changes |= Change::Tags;
}
if (auto* evt = eventCast<const ReadMarkerEvent>(event))
- changes |= d->setFullyReadMarker(evt->event_id());
+ changes |= d->setFullyReadMarker(evt->eventId());
// For all account data events
auto& currentData = d->accountData[event->matrixType()];
@@ -2751,7 +3288,10 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event)
qCDebug(STATE) << "Updated account data of type"
<< currentData->matrixType();
emit accountDataChanged(currentData->matrixType());
- changes |= Change::AccountDataChange;
+ // TODO: Drop AccountDataChange in 0.8
+ // NB: GCC (at least 10) only accepts QT_IGNORE_DEPRECATIONS around
+ // a statement, not within a statement
+ QT_IGNORE_DEPRECATIONS(changes |= Change::AccountData | Change::Other;)
}
return changes;
}
@@ -2833,7 +3373,7 @@ QString Room::Private::calculateDisplayname() const
shortlist = buildShortlist(membersLeft);
QStringList names;
- for (auto u : shortlist) {
+ for (const auto* u : shortlist) {
if (u == nullptr || isLocalUser(u))
break;
// Only disambiguate if the room is not empty
@@ -2921,41 +3461,24 @@ QJsonObject Room::Private::toJson() const
{ QStringLiteral("events"), accountDataEvents } });
}
- if (const auto& readReceiptEventId = lastReadEventIds.value(q->localUser());
- !readReceiptEventId.isEmpty()) //
+ if (const auto& readReceipt = q->lastReadReceipt(connection->userId());
+ !readReceipt.eventId.isEmpty()) //
{
- // Okay, that's a mouthful; but basically, it's simply placing an m.read
- // event in the 'ephemeral' section of the cached sync payload.
- // See also receiptevent.* and m.read example in the spec.
- // Only the local user's read receipt is saved - others' are really
- // considered ephemeral but this one is useful in understanding where
- // the user is in the timeline before any history is loaded.
result.insert(
QStringLiteral("ephemeral"),
QJsonObject {
{ QStringLiteral("events"),
- QJsonArray { QJsonObject {
- { TypeKey, ReceiptEvent::matrixTypeId() },
- { ContentKey,
- QJsonObject {
- { readReceiptEventId,
- QJsonObject {
- { QStringLiteral("m.read"),
- QJsonObject {
- { connection->userId(),
- QJsonObject {} } } } } } } } } } } });
+ QJsonArray { ReceiptEvent({ { readReceipt.eventId,
+ { { connection->userId(),
+ readReceipt.timestamp } } } })
+ .fullJson() } } });
}
- QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey,
- unreadMessages } };
-
- if (highlightCount > 0)
- unreadNotifObj.insert(QStringLiteral("highlight_count"), highlightCount);
- if (notificationCount > 0)
- unreadNotifObj.insert(QStringLiteral("notification_count"),
- notificationCount);
-
- result.insert(QStringLiteral("unread_notifications"), unreadNotifObj);
+ result.insert(UnreadNotificationsKey,
+ QJsonObject { { PartiallyReadCountKey,
+ countFromStats(partiallyReadStats) },
+ { HighlightCountKey, serverHighlightCount } });
+ result.insert(NewUnreadCountKey, countFromStats(unreadStats));
if (et.elapsed() > 30)
qCDebug(PROFILER) << "Room::toJson() for" << q->objectName() << "took"
@@ -2970,15 +3493,28 @@ MemberSorter Room::memberSorter() const { return MemberSorter(this); }
bool MemberSorter::operator()(User* u1, User* u2) const
{
- return operator()(u1, room->roomMembername(u2));
+ return operator()(u1, room->disambiguatedMemberName(u2->id()));
}
-bool MemberSorter::operator()(User* u1, const QString& u2name) const
+bool MemberSorter::operator()(User* u1, QStringView u2name) const
{
- auto n1 = room->roomMembername(u1);
+ auto n1 = room->disambiguatedMemberName(u1->id());
if (n1.startsWith('@'))
n1.remove(0, 1);
- auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0);
+ const auto n2 = u2name.mid(u2name.startsWith('@') ? 1 : 0)
+#if QT_VERSION_MAJOR < 6
+ .toString() // Qt 5 doesn't have QStringView::localeAwareCompare
+#endif
+ ;
return n1.localeAwareCompare(n2) < 0;
}
+
+void Room::activateEncryption()
+{
+ if(usesEncryption()) {
+ qCWarning(E2EE) << "Room" << objectName() << "is already encrypted";
+ return;
+ }
+ setState<EncryptionEvent>(EncryptionType::MegolmV1AesSha2);
+}