Newer
Older
// SPDX-FileCopyrightText: Nheko Contributors
#include <QApplication>
#include "AvatarProvider.h"
#include "EventAccessors.h"
#include "MainWindow.h"
#include "UserSettingsPage.h"
#include "Utils.h"
#include "encryption/DeviceVerificationFlow.h"
#include "encryption/Olm.h"
#include "ui/RoomSummary.h"
Nicolas Werner
committed
#include "ui/UserProfile.h"
#include "voip/CallManager.h"
#include "notifications/Manager.h"
#include "blurhash.hpp"
ChatPage *ChatPage::instance_ = nullptr;
static constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
static constexpr int RETRY_TIMEOUT = 5'000;
static constexpr size_t MAX_ONETIME_KEYS = 50;
ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QObject *parent)
: QObject(parent)
, userSettings_{userSettings}
, notificationsManager(new NotificationsManager(this))
, callManager_(new CallManager(this))
setObjectName(QStringLiteral("chatPage"));
view_manager_ = new TimelineViewManager(callManager_, this);
connect(this,
&ChatPage::downloadedSecrets,
this,
&ChatPage::decryptDownloadedSecrets,
Qt::QueuedConnection);
connect(this, &ChatPage::connectionLost, this, [this]() {
nhlog::net()->info("connectivity lost");
isConnected_ = false;
http::client()->shutdown();
});
connect(this, &ChatPage::connectionRestored, this, [this]() {
nhlog::net()->info("trying to re-connect");
isConnected_ = true;
// Drop all pending connections.
http::client()->shutdown();
trySync();
});
connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
connect(&connectivityTimer_, &QTimer::timeout, this, [this]() {
if (http::client()->access_token().empty()) {
connectivityTimer_.stop();
return;
}
http::client()->versions(
[this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
if (err) {
emit connectionLost();
return;
}
// only update spaces every 20 minutes
if (lastSpacesUpdate < QDateTime::currentDateTime().addSecs(-20 * 60)) {
lastSpacesUpdate = QDateTime::currentDateTime();
utils::updateSpaceVias();
if (!isConnected_)
emit connectionRestored();
});
});
connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
connect(
view_manager_,
&TimelineViewManager::inviteUsers,
this,
[this](QString roomId, QStringList users) {
for (int ii = 0; ii < users.size(); ++ii) {
QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() {
const auto user = users.at(ii);
http::client()->invite_user(
roomId.toStdString(),
user.toStdString(),
[this, user](const mtx::responses::RoomInvite &, mtx::http::RequestErr err) {
if (err) {
emit showNotification(tr("Failed to invite user: %1").arg(user));
return;
emit showNotification(tr("Invited user: %1").arg(user));
});
});
}
});
connect(this,
&ChatPage::internalKnock,
this,
qOverload<const QString &, const std::vector<std::string> &, QString, bool, bool>(
&ChatPage::knockRoom),
Qt::QueuedConnection);
connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
connect(this, &ChatPage::changeToRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
&NotificationsManager::notificationClicked,
this,
[this](const QString &roomid, const QString &eventid) {
Q_UNUSED(eventid)
auto exWin = MainWindow::instance()->windowForRoom(roomid);
if (exWin) {
exWin->setVisible(true);
exWin->raise();
exWin->requestActivate();
} else {
view_manager_->rooms()->setCurrentRoom(roomid);
MainWindow::instance()->setVisible(true);
MainWindow::instance()->raise();
MainWindow::instance()->requestActivate();
}
&NotificationsManager::sendNotificationReply,
this,
&ChatPage::sendNotificationReply);
connect(
this,
&ChatPage::initializeViews,
view_manager_,
[this](const mtx::responses::Sync &sync) { view_manager_->sync(sync); },
Qt::QueuedConnection);
connect(this,
&ChatPage::initializeEmptyViews,
view_manager_,
&TimelineViewManager::initializeRoomlist);
connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Sync &sync) {
view_manager_->sync(sync);
static unsigned int prevNotificationCount = 0;
unsigned int notificationCount = 0;
for (const auto &room : sync.rooms.join) {
notificationCount +=
static_cast<unsigned int>(room.second.unread_notifications.notification_count);
// HACK: If we had less notifications last time we checked, send an alert if the
// user wanted one. Technically, this may cause an alert to be missed if new ones
// come in while you are reading old ones. Since the window is almost certainly open
// in this edge case, that's probably a non-issue.
// TODO: Replace this once we have proper pushrules support. This is a horrible hack
if (prevNotificationCount < notificationCount) {
if (userSettings_->hasAlertOnNotification())
}
prevNotificationCount = notificationCount;
// No need to check amounts for this section, as this function internally checks for
// duplicates.
if (notificationCount && userSettings_->hasNotifications())
for (const auto &e : sync.account_data.events) {
if (auto newRules =
std::get_if<mtx::events::AccountDataEvent<mtx::pushrules::GlobalRuleset>>(&e))
pushrules =
std::make_unique<mtx::pushrules::PushRuleEvaluator>(newRules->content.global);
}
if (!pushrules) {
auto eventInDb = cache::client()->getAccountData(mtx::events::EventType::PushRules);
if (eventInDb) {
if (auto newRules =
std::get_if<mtx::events::AccountDataEvent<mtx::pushrules::GlobalRuleset>>(
&*eventInDb))
pushrules =
std::make_unique<mtx::pushrules::PushRuleEvaluator>(newRules->content.global);
}
}
if (pushrules) {
const auto local_user = utils::localUser().toStdString();
// Desktop notifications to be sent
std::vector<std::tuple<QSharedPointer<TimelineModel>,
mtx::events::collections::TimelineEvents,
std::string,
std::vector<mtx::pushrules::actions::Action>>>
notifications;
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
for (const auto &[room_id, room] : sync.rooms.join) {
// clear old notifications
for (const auto &e : room.ephemeral.events) {
if (auto receiptsEv =
std::get_if<mtx::events::EphemeralEvent<mtx::events::ephemeral::Receipt>>(
&e)) {
std::vector<QString> receipts;
for (const auto &[event_id, userReceipts] : receiptsEv->content.receipts) {
if (auto r = userReceipts.find(mtx::events::ephemeral::Receipt::Read);
r != userReceipts.end()) {
for (const auto &[user_id, receipt] : r->second.users) {
(void)receipt;
if (user_id == local_user) {
receipts.push_back(QString::fromStdString(event_id));
break;
}
}
}
if (auto r =
userReceipts.find(mtx::events::ephemeral::Receipt::ReadPrivate);
r != userReceipts.end()) {
for (const auto &[user_id, receipt] : r->second.users) {
(void)receipt;
if (user_id == local_user) {
receipts.push_back(QString::fromStdString(event_id));
break;
}
}
}
}
if (!receipts.empty())
notificationsManager->removeNotifications(
QString::fromStdString(room_id), receipts);
}
}
// calculate new notifications
if (!room.timeline.events.empty() &&
(room.unread_notifications.notification_count ||
room.unread_notifications.highlight_count)) {
auto roomModel =
view_manager_->rooms()->getRoomById(QString::fromStdString(room_id));
if (!roomModel) {
continue;
}
auto currentReadMarker =
cache::getEventIndex(room_id, cache::client()->getFullyReadEventId(room_id));
auto ctx = roomModel->pushrulesRoomContext();
std::pair<mtx::common::Relation, mtx::events::collections::TimelineEvents>>
for (const auto &event : room.timeline.events) {
auto event_id = mtx::accessors::event_id(event);
// skip already read events
if (currentReadMarker &&
currentReadMarker > cache::getEventIndex(room_id, event_id))
continue;
// skip our messages
auto sender = mtx::accessors::sender(event);
if (sender == http::client()->user_id().to_string())
continue;
mtx::events::collections::TimelineEvents te{event};
std::visit([room_id = room_id](auto &event_) { event_.room_id = room_id; },
if (auto encryptedEvent =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
&event);
encryptedEvent && userSettings_->decryptNotifications()) {
MegolmSessionIndex index(room_id, encryptedEvent->content);
auto result = olm::decryptEvent(index, *encryptedEvent);
if (result.event)
te = std::move(result.event).value();
for (const auto &r : mtx::accessors::relations(te).relations) {
auto related = cache::client()->getEvent(room_id, r.event_id);
if (related) {
relatedEvents.emplace_back(r, *related);
if (auto encryptedEvent = std::get_if<
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
encryptedEvent && userSettings_->decryptNotifications()) {
MegolmSessionIndex index(room_id, encryptedEvent->content);
auto result = olm::decryptEvent(index, *encryptedEvent);
if (result.event)
relatedEvents.back().second =
std::move(result.event).value();
}
}
}
auto actions = pushrules->evaluate(te, ctx, relatedEvents);
if (std::find(actions.begin(),
actions.end(),
mtx::pushrules::actions::Action{
mtx::pushrules::actions::notify{}}) != actions.end()) {
if (!cache::isNotificationSent(event_id)) {
// We should only send one notification per event.
cache::markSentNotification(event_id);
// Don't send a notification when the current room is opened.
if (isRoomActive(roomModel->roomId()))
continue;
if (userSettings_->hasDesktopNotifications()) {
notifications.emplace_back(roomModel, te, room_id, actions);
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
if (notifications.size() <= 5) {
for (const auto &[roomModel, te, room_id, actions] : notifications) {
AvatarProvider::resolve(
roomModel->roomAvatarUrl(),
96,
this,
[this, te = te, room_id = room_id, actions = actions](QPixmap image) {
notificationsManager->postNotification(
mtx::responses::Notification{
.actions = actions,
.event = std::move(te),
.read = false,
.profile_tag = "",
.room_id = room_id,
.ts = 0,
},
image.toImage());
});
}
} else if (!notifications.empty()) {
std::map<QSharedPointer<TimelineModel>, std::size_t> missedEvents;
for (const auto &[roomModel, te, room_id, actions] : notifications) {
missedEvents[roomModel]++;
}
QString body;
for (const auto &[roomModel, nbNotifs] : missedEvents) {
body += tr("%n unread message(s) in room %1\n", nullptr, nbNotifs)
.arg(roomModel->roomName());
}
emit notificationsManager->systemPostNotificationCb(
"", "", "New messages while away", body, QImage());
}
});
connect(
this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection);
connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync, Qt::QueuedConnection);
connect(
this,
&ChatPage::tryDelayedSyncCb,
this,
[this]() { QTimer::singleShot(RETRY_TIMEOUT, this, &ChatPage::trySync); },
Qt::QueuedConnection);
connect(
this, &ChatPage::newSyncResponse, this, &ChatPage::handleSyncResponse, Qt::QueuedConnection);
connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
connect(
this,
&ChatPage::startRemoveFallbackKeyTimer,
this,
[this]() {
QTimer::singleShot(std::chrono::minutes(5), this, &ChatPage::removeOldFallbackKey);
disconnect(
this, &ChatPage::newSyncResponse, this, &ChatPage::startRemoveFallbackKeyTimer);
connect(
this,
&ChatPage::callFunctionOnGuiThread,
this,
[](std::function<void()> f) { f(); },
Qt::QueuedConnection);
connectCallMessage<mtx::events::voip::CallInvite>();
connectCallMessage<mtx::events::voip::CallCandidates>();
connectCallMessage<mtx::events::voip::CallAnswer>();
connectCallMessage<mtx::events::voip::CallHangUp>();
connectCallMessage<mtx::events::voip::CallSelectAnswer>();
connectCallMessage<mtx::events::voip::CallReject>();
connectCallMessage<mtx::events::voip::CallNegotiate>();
emit closing();
connectivityTimer_.stop();
}
void
ChatPage::dropToLoginPage(const QString &msg)
{
nhlog::ui()->info("dropping to the login page: {}", msg.toStdString());
http::client()->shutdown();
connectivityTimer_.stop();
auto btn = QMessageBox::warning(
nullptr,
tr("Confirm logout"),
tr("Because of the following reason Nheko wants to drop you to the login page:\n%1\nIf you "
"think this is a mistake, you can close Nheko instead to possibly recover your encryption "
"keys. After you have been dropped to the login page, you can sign in again using your "
"usual methods.")
.arg(msg),
QMessageBox::StandardButton::Close | QMessageBox::StandardButton::Ok,
QMessageBox::StandardButton::Ok);
if (btn == QMessageBox::StandardButton::Close) {
QCoreApplication::exit(1);
exit(1);
}
}
void
ChatPage::resetUI()
{
}
void
ChatPage::deleteConfigs()
{
auto settings = UserSettings::instance()->qsettings();
if (UserSettings::instance()->profile() != QLatin1String("")) {
settings->beginGroup(QStringLiteral("profile"));
settings->beginGroup(UserSettings::instance()->profile());
}
settings->beginGroup(QStringLiteral("auth"));
settings->remove(QLatin1String(""));
if (UserSettings::instance()->profile() != QLatin1String("")) {
settings->endGroup(); // profilename
settings->endGroup(); // profile
}
http::client()->shutdown();
cache::deleteData();
void
ChatPage::bootstrap(QString userid, QString homeserver, QString token)
try {
http::client()->set_user(parse<User>(userid.toStdString()));
} catch (const std::invalid_argument &) {
nhlog::ui()->critical("bootstrapped with invalid user_id: {}", userid.toStdString());
}
http::client()->set_server(homeserver.toStdString());
http::client()->set_access_token(token.toStdString());
http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation());
// The Olm client needs the user_id & device_id that will be included
// in the generated payloads & keys.
olm::client()->set_user_id(http::client()->user_id().to_string());
olm::client()->set_device_id(http::client()->device_id());
connect(cache::client(), &Cache::databaseReady, this, [this]() {
nhlog::db()->info("database ready");
const bool isInitialized = cache::isInitialized();
const auto cacheVersion = cache::formatVersion();
try {
if (!isInitialized) {
cache::setCurrentFormat();
} else {
if (cacheVersion == cache::CacheVersion::Current) {
loadStateFromCache();
return;
} else if (cacheVersion == cache::CacheVersion::Older) {
if (!cache::runMigrations()) {
QMessageBox::critical(
tr("Cache migration failed!"),
tr("Migrating the cache to the current version failed. "
"This can have different reasons. Please open an "
"issue at https://github.com/Nheko-Reborn/nheko and try to use an "
"older version in the meantime. Alternatively you can try "
"deleting the cache manually."));
QCoreApplication::quit();
}
loadStateFromCache();
return;
} else if (cacheVersion == cache::CacheVersion::Newer) {
QMessageBox::critical(
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
tr("Incompatible cache version"),
tr("The cache on your disk is newer than this version of Nheko "
"supports. Please update Nheko or clear your cache."));
QCoreApplication::quit();
return;
}
}
// It's the first time syncing with this device
// There isn't a saved olm account to restore.
nhlog::crypto()->info("creating new olm account");
olm::client()->create_new_account();
cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret()));
} catch (const lmdb::error &e) {
nhlog::crypto()->critical("failed to save olm account {}", e.what());
emit dropToLoginPageCb(QString::fromStdString(e.what()));
return;
} catch (const mtx::crypto::olm_exception &e) {
nhlog::crypto()->critical("failed to create new olm account {}", e.what());
emit dropToLoginPageCb(QString::fromStdString(e.what()));
return;
}
getProfileInfo();
getBackupVersion();
tryInitialSync();
callManager_->refreshTurnServer();
emit MainWindow::instance()->reload();
connect(cache::client(),
&Cache::newReadReceipts,
view_manager_,
&TimelineViewManager::updateReadReceipts);
connect(cache::client(), &Cache::secretChanged, this, [this](const std::string &secret) {
if (secret == mtx::secret_storage::secrets::megolm_backup_v1) {
getBackupVersion();
}
});
} catch (const lmdb::error &e) {
nhlog::db()->critical("failure during boot: {}", e.what());
emit dropToLoginPageCb(tr("Failed to open database, logging out!"));
nhlog::db()->info("restoring state from cache");
try {
olm::client()->load(cache::restoreOlmAccount(), cache::client()->pickleSecret());
emit initializeEmptyViews();
cache::calculateRoomReadStatus();
} catch (const mtx::crypto::olm_exception &e) {
nhlog::crypto()->critical("failed to restore olm account: {}", e.what());
emit dropToLoginPageCb(tr("Failed to restore OLM account. Please login again."));
return;
} catch (const lmdb::error &e) {
nhlog::db()->critical("failed to restore cache: {}", e.what());
emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
return;
} catch (const nlohmann::json::exception &e) {
nhlog::db()->critical("failed to parse cache data: {}", e.what());
emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
return;
} catch (const std::exception &e) {
nhlog::db()->critical("failed to load cache data: {}", e.what());
emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
return;
}
nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519);
nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
getProfileInfo();
getBackupVersion();
verifyOneTimeKeyCountAfterStartup();
callManager_->refreshTurnServer();
emit contentLoaded();
// Start receiving events.
connect(this, &ChatPage::newSyncResponse, &ChatPage::startRemoveFallbackKeyTimer);
try {
cache::removeRoom(room_id);
cache::removeInvite(room_id.toStdString());
} catch (const lmdb::error &e) {
nhlog::db()->critical("failure while removing room: {}", e.what());
// TODO: Notify the user.
}
void
ChatPage::tryInitialSync()
{
nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519);
nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
// Upload one time keys for the device.
nhlog::crypto()->info("generating one time keys");
olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS, true);
http::client()->upload_keys(
olm::client()->create_upload_keys_request(),
[this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) {
if (err) {
const int status_code = static_cast<int>(err->status_code);
Konstantinos Sideris
committed
if (status_code == 404) {
nhlog::net()->warn("skipping key uploading. server doesn't provide /keys/upload");
return startInitialSync();
}
nhlog::crypto()->critical("failed to upload one time keys: {}", err);
Konstantinos Sideris
committed
QString errorMsg(tr("Failed to setup encryption keys. Server response: "
"%1 %2. Please try again later.")
.arg(QString::fromStdString(err->matrix_error.error))
.arg(status_code));
Konstantinos Sideris
committed
emit dropToLoginPageCb(errorMsg);
return;
}
olm::client()->forget_old_fallback_key();
for (const auto &entry : res.one_time_key_counts)
nhlog::net()->info("uploaded {} {} one-time keys", entry.second, entry.first);
cache::client()->markUserKeysOutOfDate({http::client()->user_id().to_string()});
void
ChatPage::startInitialSync()
{
nhlog::net()->info("trying initial sync");
mtx::http::SyncOpts opts;
opts.timeout = 0;
opts.set_presence = currentPresence();
http::client()->sync(opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
// TODO: Initial Sync should include mentions as well...
if (err) {
const auto error = QString::fromStdString(err->matrix_error.error);
const auto msg = tr("Please try to login again: %1").arg(error);
const auto err_code = mtx::errors::to_string(err->matrix_error.errcode);
const int status_code = static_cast<int>(err->status_code);
nhlog::net()->error("initial sync error: {}", err);
// non http related errors
if (status_code <= 0 || status_code >= 600) {
startInitialSync();
return;
}
switch (status_code) {
case 502:
case 504:
case 524: {
startInitialSync();
return;
}
default: {
emit dropToLoginPageCb(msg);
return;
}
}
}
QTimer::singleShot(0, this, [this, res] {
nhlog::net()->info("initial sync completed");
try {
cache::client()->saveState(res);
olm::handle_to_device_messages(res.to_device.events);
emit initializeViews(std::move(res));
cache::calculateRoomReadStatus();
} catch (const lmdb::error &e) {
nhlog::db()->error("failed to save state after initial sync: {}", e.what());
startInitialSync();
return;
}
emit trySyncCb();
emit contentLoaded();
});
ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token)
try {
if (prev_batch_token != cache::nextBatchToken()) {
nhlog::net()->warn("Duplicate sync, dropping");
return;
nhlog::db()->warn("Logged out in the mean time, dropping sync");
nhlog::net()->debug("sync completed: {}", res.next_batch);
// Ensure that we have enough one-time keys available.
ensureOneTimeKeyCount(res.device_one_time_keys_count, res.device_unused_fallback_key_types);
// TODO: fine grained error handling
try {
cache::client()->saveState(res);
olm::handle_to_device_messages(res.to_device.events);
auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res));
// if we process a lot of syncs (1 every 200ms), this means we clean the
// db every 100s
static int syncCounter = 0;
if (syncCounter++ >= 500) {
cache::deleteOldData();
syncCounter = 0;
} catch (const lmdb::map_full_error &e) {
nhlog::db()->error("lmdb is full: {}", e.what());
cache::deleteOldData();
} catch (const lmdb::error &e) {
nhlog::db()->error("saving sync response: {}", e.what());
}
emit trySyncCb();
void
ChatPage::trySync()
{
mtx::http::SyncOpts opts;
opts.set_presence = currentPresence();
if (!connectivityTimer_.isActive())
connectivityTimer_.start();
try {
opts.since = cache::nextBatchToken();
} catch (const lmdb::error &e) {
nhlog::db()->error("failed to retrieve next batch token: {}", e.what());
return;
}
http::client()->sync(
opts, [this, since = opts.since](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
if (err) {
const auto error = QString::fromStdString(err->matrix_error.error);
const auto msg = tr("Please try to login again: %1").arg(error);
if ((http::is_logged_in() &&
(err->matrix_error.errcode == mtx::errors::ErrorCode::M_UNKNOWN_TOKEN ||
err->matrix_error.errcode == mtx::errors::ErrorCode::M_MISSING_TOKEN)) ||
!http::is_logged_in()) {
emit dropToLoginPageCb(msg);
return;
}
emit tryDelayedSyncCb();
return;
}
emit newSyncResponse(res, since);
});
ChatPage::knockRoom(const QString &room,
const std::vector<std::string> &via,
QString reason,
bool failedJoin,
bool promptForConfirmation)
bool confirmed = false;
if (promptForConfirmation) {
reason = QInputDialog::getText(
nullptr,
tr("Knock on room"),
// clang-format off
? tr("You failed to join %1. You can try to knock so that others can invite you in. Do you want to do so?\nYou may optionally provide a reason for others to accept your knock:").arg(room)
: tr("Do you really want to knock on %1? You may optionally provide a reason for others to accept your knock:").arg(room),
// clang-format on
QLineEdit::Normal,
reason,
&confirmed);
if (!confirmed) {
return;
}
[this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
if (err) {
emit showNotification(tr("Failed to knock room: %1")
.arg(QString::fromStdString(err->matrix_error.error)));
return;
}
ChatPage::joinRoom(const QString &room, const QString &reason)
const auto room_id = room.toStdString();
ChatPage::joinRoomVia(const std::string &room_id,
const std::vector<std::string> &via,
bool promptForConfirmation,
const QString &reason)
if (promptForConfirmation) {
auto prompt = new RoomSummary(room_id, via, reason);
QQmlEngine::setObjectOwnership(prompt, QQmlEngine::JavaScriptOwnership);
emit showRoomJoinPrompt(prompt);
[this, room_id, reason, via](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
if (err->matrix_error.errcode == mtx::errors::ErrorCode::M_FORBIDDEN)
emit internalKnock(QString::fromStdString(room_id), via, reason, true, true);
else
emit showNotification(tr("Failed to join room: %1")
.arg(QString::fromStdString(err->matrix_error.error)));
return;
}
// We remove any invites with the same room_id.
try {
cache::removeInvite(room_id);
} catch (const lmdb::error &e) {
emit showNotification(tr("Failed to remove invite: %1").arg(e.what()));
}
view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
}
void
ChatPage::createRoom(const mtx::requests::CreateRoom &req)
{
if (req.room_alias_name.find(":") != std::string::npos ||
req.room_alias_name.find("#") != std::string::npos) {
nhlog::net()->warn("Failed to create room: Some characters are not allowed in alias");
emit this->showNotification(tr("Room creation failed: Bad Alias"));
return;
}
http::client()->create_room(
req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) {
if (err) {
const auto err_code = mtx::errors::to_string(err->matrix_error.errcode);
const auto error = err->matrix_error.error;
nhlog::net()->warn("failed to create room: {})", err);
emit showNotification(
tr("Room creation failed: %1").arg(QString::fromStdString(error)));
return;
}
QString newRoomId = QString::fromStdString(res.room_id.to_string());
emit showNotification(tr("Room %1 created.").arg(newRoomId));
emit newRoom(newRoomId);
emit changeToRoom(newRoomId);
});
ChatPage::leaveRoom(const QString &room_id, const QString &reason)
http::client()->leave_room(
room_id.toStdString(),
[this, room_id](const mtx::responses::Empty &, mtx::http::RequestErr err) {
if (err) {
emit showNotification(tr("Failed to leave room: %1")
.arg(QString::fromStdString(err->matrix_error.error)));
nhlog::net()->error("Failed to leave room '{}': {}", room_id.toStdString(), err);
if (err->status_code == 404 &&
err->matrix_error.errcode == mtx::errors::ErrorCode::M_UNKNOWN) {
nhlog::db()->debug(
"Removing invite and room for {}, even though we couldn't leave.",
room_id.toStdString());
cache::client()->removeInvite(room_id.toStdString());
cache::client()->removeRoom(room_id.toStdString());
}
return;
}
emit leftRoom(room_id);
void
ChatPage::changeRoom(const QString &room_id)
{
view_manager_->rooms()->setCurrentRoom(room_id);
void
ChatPage::inviteUser(const QString &room, QString userid, QString reason)
tr("Confirm invite"),
tr("Do you really want to invite %1 (%2)?")
.arg(cache::displayName(room, userid), userid)) != QMessageBox::Yes)
return;
http::client()->invite_user(
room.toStdString(),
userid.toStdString(),
[this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->error(
"Failed to invite {} to {}: {}", userid.toStdString(), room.toStdString(), *err);
emit showNotification(
tr("Failed to invite %1 to %2: %3")
.arg(userid, room, QString::fromStdString(err->matrix_error.error)));
} else
emit showNotification(tr("Invited user: %1").arg(userid));
},
reason.trimmed().toStdString());