summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorKonstantinos Sideris <sideris.konstantin@gmail.com>2017-07-29 11:49:00 +0300
committerKonstantinos Sideris <sideris.konstantin@gmail.com>2017-07-30 14:30:55 +0300
commit8d05073547d02e105712713de39e8359980812c2 (patch)
tree4a806e19b70f4b95f242752b7d7f601c05376026 /src
parentAdd serialization on matrix events (diff)
downloadnheko-8d05073547d02e105712713de39e8359980812c2.tar.xz
Initial support for state cache
- Adds detection for duplicate events
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cc229
-rw-r--r--src/ChatPage.cc106
-rw-r--r--src/MainWindow.cc5
-rw-r--r--src/RoomState.cc139
-rw-r--r--src/TimelineView.cc52
-rw-r--r--src/TimelineViewManager.cc19
6 files changed, 536 insertions, 14 deletions
diff --git a/src/Cache.cc b/src/Cache.cc
new file mode 100644

index 00000000..c9f3fa5f --- /dev/null +++ b/src/Cache.cc
@@ -0,0 +1,229 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdexcept> + +#include <QDebug> +#include <QDir> +#include <QFile> +#include <QStandardPaths> + +#include "Cache.h" +#include "MemberEventContent.h" + +namespace events = matrix::events; + +static const lmdb::val NEXT_BATCH_KEY("next_batch"); +static const lmdb::val transactionID("transaction_id"); + +Cache::Cache(const QString &userId) + : env_{nullptr} + , stateDb_{0} + , roomDb_{0} + , isMounted_{false} + , userId_{userId} +{ + auto statePath = QString("%1/%2/state") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString::fromUtf8(userId_.toUtf8().toHex())); + + bool isInitial = !QFile::exists(statePath); + + env_ = lmdb::env::create(); + env_.set_mapsize(128UL * 1024UL * 1024UL); /* 128 MB */ + env_.set_max_dbs(1024UL); + + if (isInitial) { + qDebug() << "[cache] First time initializing LMDB"; + + if (!QDir().mkpath(statePath)) { + throw std::runtime_error(("Unable to create state directory:" + statePath).toStdString().c_str()); + } + } + + try { + env_.open(statePath.toStdString().c_str()); + } catch (const lmdb::error &e) { + if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) { + throw std::runtime_error("LMDB initialization failed" + std::string(e.what())); + } + + qWarning() << "Resetting cache due to LMDB version mismatch:" << e.what(); + + QDir stateDir(statePath); + + for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) { + if (!stateDir.remove(file)) + throw std::runtime_error(("Unable to delete file " + file).toStdString().c_str()); + } + + env_.open(statePath.toStdString().c_str()); + } + + auto txn = lmdb::txn::begin(env_); + stateDb_ = lmdb::dbi::open(txn, "state", MDB_CREATE); + roomDb_ = lmdb::dbi::open(txn, "rooms", MDB_CREATE); + + txn.commit(); + + isMounted_ = true; +} + +void Cache::insertRoomState(const QString &roomid, const RoomState &state) +{ + if (!isMounted_) + return; + + auto txn = lmdb::txn::begin(env_); + + auto stateEvents = QJsonDocument(state.serialize()).toBinaryData(); + auto id = roomid.toUtf8(); + + lmdb::dbi_put( + txn, + roomDb_, + lmdb::val(id.data(), id.size()), + lmdb::val(stateEvents.data(), stateEvents.size())); + + for (const auto &membership : state.memberships) { + lmdb::dbi membersDb = lmdb::dbi::open(txn, roomid.toStdString().c_str(), MDB_CREATE); + + // The user_id this membership event relates to, is used + // as the index on the membership database. + auto key = membership.stateKey().toUtf8(); + auto memberEvent = QJsonDocument(membership.serialize()).toBinaryData(); + + switch (membership.content().membershipState()) { + // We add or update (e.g invite -> join) a new user to the membership list. + case events::Membership::Invite: + case events::Membership::Join: { + lmdb::dbi_put( + txn, + membersDb, + lmdb::val(key.data(), key.size()), + lmdb::val(memberEvent.data(), memberEvent.size())); + break; + } + // We remove the user from the membership list. + case events::Membership::Leave: + case events::Membership::Ban: { + lmdb::dbi_del( + txn, + membersDb, + lmdb::val(key.data(), key.size()), + lmdb::val(memberEvent.data(), memberEvent.size())); + break; + } + case events::Membership::Knock: { + qWarning() << "Skipping knock membership" << roomid << key; + break; + } + } + } + + txn.commit(); +} + +QMap<QString, RoomState> Cache::states() +{ + QMap<QString, RoomState> states; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto cursor = lmdb::cursor::open(txn, roomDb_); + + std::string room; + std::string stateData; + + // Retrieve all the room names. + while (cursor.get(room, stateData, MDB_NEXT)) { + auto roomid = QString::fromUtf8(room.data(), room.size()); + auto json = QJsonDocument::fromBinaryData(QByteArray(stateData.data(), stateData.size())); + + RoomState state; + state.parse(json.object()); + + auto memberDb = lmdb::dbi::open(txn, roomid.toStdString().c_str(), MDB_CREATE); + QMap<QString, events::StateEvent<events::MemberEventContent>> members; + + auto memberCursor = lmdb::cursor::open(txn, memberDb); + + std::string memberId; + std::string memberContent; + + while (memberCursor.get(memberId, memberContent, MDB_NEXT)) { + auto userid = QString::fromUtf8(memberId.data(), memberId.size()); + auto data = QJsonDocument::fromBinaryData(QByteArray(memberContent.data(), memberContent.size())); + + try { + events::StateEvent<events::MemberEventContent> member; + member.deserialize(data.object()); + members.insert(userid, member); + } catch (const DeserializationException &e) { + qWarning() << e.what(); + qWarning() << "Fault while parsing member event" << data.object(); + continue; + } + } + + qDebug() << members.size() << "members for" << roomid; + + state.memberships = members; + states.insert(roomid, state); + } + + qDebug() << "Retrieved" << states.size() << "rooms"; + + cursor.close(); + + txn.commit(); + + return states; +} + +void Cache::setNextBatchToken(const QString &token) +{ + auto txn = lmdb::txn::begin(env_); + auto value = token.toUtf8(); + + lmdb::dbi_put(txn, stateDb_, NEXT_BATCH_KEY, lmdb::val(value.data(), value.size())); + + txn.commit(); +} + +bool Cache::isInitialized() +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + lmdb::val token; + + bool res = lmdb::dbi_get(txn, stateDb_, NEXT_BATCH_KEY, token); + + txn.commit(); + + return res; +} + +QString Cache::nextBatchToken() +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + lmdb::val token; + + lmdb::dbi_get(txn, stateDb_, NEXT_BATCH_KEY, token); + + txn.commit(); + + return QString::fromUtf8(token.data<const char>()); +} diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index 4e9120d2..5a5a497e 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc
@@ -46,7 +46,6 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, QWidget *parent) , sync_interval_(2000) , client_(client) { - resize(798, 519); setStyleSheet("background-color: #f8fbfe;"); topLayout_ = new QHBoxLayout(this); @@ -213,13 +212,22 @@ void ChatPage::logout() void ChatPage::bootstrap(QString userid, QString homeserver, QString token) { - Q_UNUSED(userid); - client_->setServer(homeserver); client_->setAccessToken(token); - client_->getOwnProfile(); - client_->initialSync(); + + try { + cache_ = QSharedPointer<Cache>(new Cache(userid)); + } catch (const std::exception &e) { + qCritical() << e.what(); + } catch (const lmdb::error &e) { + qCritical() << e.what(); + } + + if (cache_->isInitialized()) + loadStateFromCache(); + else + client_->initialSync(); } void ChatPage::startSync() @@ -251,6 +259,8 @@ void ChatPage::updateDisplayNames(const RoomState &state) void ChatPage::syncCompleted(const SyncResponse &response) { + // TODO: Catch exception + cache_->setNextBatchToken(response.nextBatch()); client_->setNextBatchToken(response.nextBatch()); auto joined = response.rooms().join(); @@ -258,6 +268,7 @@ void ChatPage::syncCompleted(const SyncResponse &response) for (auto it = joined.constBegin(); it != joined.constEnd(); it++) { RoomState room_state; + // Merge the new updates for rooms that we are tracking. if (state_manager_.contains(it.key())) room_state = state_manager_[it.key()]; @@ -265,6 +276,15 @@ void ChatPage::syncCompleted(const SyncResponse &response) updateRoomState(room_state, it.value().timeline().events()); updateDisplayNames(room_state); + try { + cache_->insertRoomState(it.key(), room_state); + } catch (const lmdb::error &e) { + qCritical() << e.what(); + // Stop using the cache if an errors occurs. + // TODO: Should also be marked as invalid and be deleted. + cache_->unmount(); + } + if (state_manager_.contains(it.key())) { // TODO: Use pointers instead of copying. auto oldState = state_manager_[it.key()]; @@ -291,16 +311,32 @@ void ChatPage::initialSyncCompleted(const SyncResponse &response) auto joined = response.rooms().join(); + // TODO: Catch exception + cache_->setNextBatchToken(response.nextBatch()); + for (auto it = joined.constBegin(); it != joined.constEnd(); it++) { RoomState room_state; + // Build the current state from the timeline and state events. updateRoomState(room_state, it.value().state().events()); updateRoomState(room_state, it.value().timeline().events()); + // Remove redundant memberships. room_state.removeLeaveMemberships(); + + // Resolve room name and avatar. e.g in case of one-to-one chats. room_state.resolveName(); room_state.resolveAvatar(); + try { + cache_->insertRoomState(it.key(), room_state); + } catch (const lmdb::error &e) { + qCritical() << e.what(); + // Stop using the cache if an errors occurs. + // TODO: Should also be marked as invalid and be deleted. + cache_->unmount(); + } + updateDisplayNames(room_state); state_manager_.insert(it.key(), room_state); @@ -315,10 +351,15 @@ void ChatPage::initialSyncCompleted(const SyncResponse &response) } } + // Populate timelines with messages. view_manager_->initialize(response.rooms()); + + // Initialize room list. room_list_->setInitialRooms(settingsManager_, state_manager_); sync_timer_->start(sync_interval_); + + emit contentLoaded(); } void ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img) @@ -463,6 +504,61 @@ void ChatPage::updateRoomState(RoomState &room_state, const QJsonArray &events) } } +void ChatPage::loadStateFromCache() +{ + qDebug() << "Restoring state from cache"; + + try { + qDebug() << "Restored nextBatchToken" << cache_->nextBatchToken(); + client_->setNextBatchToken(cache_->nextBatchToken()); + } catch (const lmdb::error &e) { + qCritical() << "Failed to load next_batch_token from cache" << e.what(); + // TODO: Clean the environment + return; + } + + // Fetch all the joined room's state. + auto rooms = cache_->states(); + + for (auto it = rooms.constBegin(); it != rooms.constEnd(); it++) { + RoomState room_state = it.value(); + + // Clean up and prepare state for use. + room_state.removeLeaveMemberships(); + room_state.resolveName(); + room_state.resolveAvatar(); + + // Update the global list with user's display names. + updateDisplayNames(room_state); + + // Save the current room state. + state_manager_.insert(it.key(), room_state); + + // Create or restore the settings for this room. + settingsManager_.insert(it.key(), QSharedPointer<RoomSettings>(new RoomSettings(it.key()))); + + // Resolve user avatars. + for (const auto membership : room_state.memberships) { + auto uid = membership.sender(); + auto url = membership.content().avatarUrl(); + + if (!url.toString().isEmpty()) + AvatarProvider::setAvatarUrl(uid, url); + } + } + + // Initializing empty timelines. + view_manager_->initialize(rooms.keys()); + + // Initialize room list from the restored state and settings. + room_list_->setInitialRooms(settingsManager_, state_manager_); + + // Remove the spinner overlay. + emit contentLoaded(); + + sync_timer_->start(sync_interval_); +} + ChatPage::~ChatPage() { sync_timer_->stop(); diff --git a/src/MainWindow.cc b/src/MainWindow.cc
index 649064b8..d7e2a3c0 100644 --- a/src/MainWindow.cc +++ b/src/MainWindow.cc
@@ -76,10 +76,7 @@ MainWindow::MainWindow(QWidget *parent) this, SLOT(iconActivated(QSystemTrayIcon::ActivationReason))); - connect(client_.data(), - SIGNAL(initialSyncCompleted(const SyncResponse &)), - this, - SLOT(removeOverlayProgressBar())); + connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar())); connect(client_.data(), SIGNAL(loginSuccess(QString, QString, QString)), diff --git a/src/RoomState.cc b/src/RoomState.cc
index 3eaff452..c5e763e7 100644 --- a/src/RoomState.cc +++ b/src/RoomState.cc
@@ -16,6 +16,7 @@ */ #include <QDebug> +#include <QJsonArray> #include <QSettings> #include "RoomState.h" @@ -150,3 +151,141 @@ void RoomState::update(const RoomState &state) if (needsAvatarCalculation) resolveAvatar(); } + +QJsonObject RoomState::serialize() const +{ + QJsonObject obj; + + if (!aliases.eventId().isEmpty()) + obj["aliases"] = aliases.serialize(); + + if (!avatar.eventId().isEmpty()) + obj["avatar"] = avatar.serialize(); + + if (!canonical_alias.eventId().isEmpty()) + obj["canonical_alias"] = canonical_alias.serialize(); + + if (!create.eventId().isEmpty()) + obj["create"] = create.serialize(); + + if (!history_visibility.eventId().isEmpty()) + obj["history_visibility"] = history_visibility.serialize(); + + if (!join_rules.eventId().isEmpty()) + obj["join_rules"] = join_rules.serialize(); + + if (!name.eventId().isEmpty()) + obj["name"] = name.serialize(); + + if (!power_levels.eventId().isEmpty()) + obj["power_levels"] = power_levels.serialize(); + + if (!topic.eventId().isEmpty()) + obj["topic"] = topic.serialize(); + + return obj; +} + +void RoomState::parse(const QJsonObject &object) +{ + // FIXME: Make this less versbose. + + if (object.contains("aliases")) { + events::StateEvent<events::AliasesEventContent> event; + + try { + event.deserialize(object["aliases"]); + aliases = event; + } catch (const DeserializationException &e) { + qWarning() << "RoomState::parse - aliases" << e.what(); + } + } + + if (object.contains("avatar")) { + events::StateEvent<events::AvatarEventContent> event; + + try { + event.deserialize(object["avatar"]); + avatar = event; + } catch (const DeserializationException &e) { + qWarning() << "RoomState::parse - avatar" << e.what(); + } + } + + if (object.contains("canonical_alias")) { + events::StateEvent<events::CanonicalAliasEventContent> event; + + try { + event.deserialize(object["canonical_alias"]); + canonical_alias = event; + } catch (const DeserializationException &e) { + qWarning() << "RoomState::parse - canonical_alias" << e.what(); + } + } + + if (object.contains("create")) { + events::StateEvent<events::CreateEventContent> event; + + try { + event.deserialize(object["create"]); + create = event; + } catch (const DeserializationException &e) { + qWarning() << "RoomState::parse - create" << e.what(); + } + } + + if (object.contains("history_visibility")) { + events::StateEvent<events::HistoryVisibilityEventContent> event; + + try { + event.deserialize(object["history_visibility"]); + history_visibility = event; + } catch (const DeserializationException &e) { + qWarning() << "RoomState::parse - history_visibility" << e.what(); + } + } + + if (object.contains("join_rules")) { + events::StateEvent<events::JoinRulesEventContent> event; + + try { + event.deserialize(object["join_rules"]); + join_rules = event; + } catch (const DeserializationException &e) { + qWarning() << "RoomState::parse - join_rules" << e.what(); + } + } + + if (object.contains("name")) { + events::StateEvent<events::NameEventContent> event; + + try { + event.deserialize(object["name"]); + name = event; + } catch (const DeserializationException &e) { + qWarning() << "RoomState::parse - name" << e.what(); + } + } + + if (object.contains("power_levels")) { + events::StateEvent<events::PowerLevelsEventContent> event; + + try { + event.deserialize(object["power_levels"]); + power_levels = event; + } catch (const DeserializationException &e) { + qWarning() << "RoomState::parse - power_levels" << e.what(); + } + } + + if (object.contains("topic")) { + events::StateEvent<events::TopicEventContent> event; + + try { + event.deserialize(object["topic"]); + topic = event; + } catch (const DeserializationException &e) { + qWarning() << "RoomState::parse - topic" << e.what(); + } + } +} diff --git a/src/TimelineView.cc b/src/TimelineView.cc
index 3f7c877e..731e7db5 100644 --- a/src/TimelineView.cc +++ b/src/TimelineView.cc
@@ -49,6 +49,18 @@ TimelineView::TimelineView(const Timeline &timeline, addEvents(timeline); } +TimelineView::TimelineView(QSharedPointer<MatrixClient> client, const QString &room_id, QWidget *parent) + : QWidget(parent) + , room_id_{room_id} + , client_{client} +{ + QSettings settings; + local_user_ = settings.value("auth/user_id").toString(); + + init(); + client_->messages(room_id_, ""); +} + void TimelineView::sliderRangeChanged(int min, int max) { Q_UNUSED(min); @@ -64,8 +76,26 @@ void TimelineView::sliderRangeChanged(int min, int max) int currentHeight = scroll_widget_->size().height(); int diff = currentHeight - oldHeight_; + int newPosition = oldPosition_ + diff; + + // Keep the scroll bar to the bottom if we are coming from + // an scrollbar without height i.e scrollbar->value() == 0 + if (oldPosition_ == 0) + newPosition = max; + + scroll_area_->verticalScrollBar()->setValue(newPosition); + fetchHistory(); + } +} - scroll_area_->verticalScrollBar()->setValue(oldPosition_ + diff); +void TimelineView::fetchHistory() +{ + bool hasEnoughMessages = scroll_area_->verticalScrollBar()->value() != 0; + + if (!hasEnoughMessages && !isTimelineFinished && !isPaginationInProgress_) { + isPaginationInProgress_ = true; + client_->messages(room_id_, prev_batch_token_); + scroll_area_->verticalScrollBar()->setValue(scroll_area_->verticalScrollBar()->maximum()); } } @@ -139,8 +169,10 @@ void TimelineView::addBackwardsEvents(const QString &room_id, const RoomMessages oldPosition_ = scroll_area_->verticalScrollBar()->value(); oldHeight_ = scroll_widget_->size().height(); - for (const auto &item : items) + for (const auto &item : items) { + item->adjustSize(); addTimelineItem(item, TimelineDirection::Top); + } prev_batch_token_ = msgs.end(); isPaginationInProgress_ = false; @@ -164,6 +196,11 @@ TimelineItem *TimelineView::parseMessageEvent(const QJsonObject &event, Timeline return nullptr; } + if (isDuplicate(text.eventId())) + return nullptr; + + eventIds_[text.eventId()] = true; + if (isPendingMessage(text, local_user_)) { removePendingMessage(text); return nullptr; @@ -186,6 +223,12 @@ TimelineItem *TimelineView::parseMessageEvent(const QJsonObject &event, Timeline return nullptr; } + if (isDuplicate(notice.eventId())) + return nullptr; + ; + + eventIds_[notice.eventId()] = true; + auto with_sender = isSenderRendered(notice.sender(), direction); updateLastSender(notice.sender(), direction); @@ -203,6 +246,11 @@ TimelineItem *TimelineView::parseMessageEvent(const QJsonObject &event, Timeline return nullptr; } + if (isDuplicate(img.eventId())) + return nullptr; + + eventIds_[img.eventId()] = true; + auto with_sender = isSenderRendered(img.sender(), direction); updateLastSender(img.sender(), direction); diff --git a/src/TimelineViewManager.cc b/src/TimelineViewManager.cc
index f55d4868..3715d1b6 100644 --- a/src/TimelineViewManager.cc +++ b/src/TimelineViewManager.cc
@@ -85,6 +85,18 @@ void TimelineViewManager::initialize(const Rooms &rooms) } } +void TimelineViewManager::initialize(const QList<QString> &rooms) +{ + for (const auto &roomid : rooms) { + // Create a history view without any events. + TimelineView *view = new TimelineView(client_, roomid); + views_.insert(roomid, QSharedPointer<TimelineView>(view)); + + // Add the view in the widget stack. + addWidget(view); + } +} + void TimelineViewManager::sync(const Rooms &rooms) { for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) { @@ -118,11 +130,12 @@ void TimelineViewManager::setHistoryView(const QString &room_id) } active_room_ = room_id; - auto widget = views_.value(room_id); + auto view = views_.value(room_id); - setCurrentWidget(widget.data()); + setCurrentWidget(view.data()); - widget->scrollDown(); + view->fetchHistory(); + view->scrollDown(); } QMap<QString, QString> TimelineViewManager::NICK_COLORS;