diff --git a/src/AvatarProvider.cc b/src/AvatarProvider.cc
index 49e52a82..391f57d9 100644
--- a/src/AvatarProvider.cc
+++ b/src/AvatarProvider.cc
@@ -16,17 +16,17 @@
*/
#include <QBuffer>
-#include <QtConcurrent>
+#include <memory>
#include "AvatarProvider.h"
#include "Cache.h"
+#include "Logging.hpp"
#include "MatrixClient.h"
+namespace AvatarProvider {
+
void
-AvatarProvider::resolve(const QString &room_id,
- const QString &user_id,
- QObject *receiver,
- std::function<void(QImage)> callback)
+resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback callback)
{
const auto key = QString("%1 %2").arg(room_id).arg(user_id);
const auto avatarUrl = Cache::avatarUrl(room_id, user_id);
@@ -43,24 +43,30 @@ AvatarProvider::resolve(const QString &room_id,
return;
}
- auto proxy = http::client()->fetchUserAvatar(avatarUrl);
+ auto proxy = std::make_shared<AvatarProxy>();
+ QObject::connect(proxy.get(),
+ &AvatarProxy::avatarDownloaded,
+ receiver,
+ [callback](const QByteArray &data) { callback(QImage::fromData(data)); });
- if (proxy.isNull())
- return;
+ mtx::http::ThumbOpts opts;
+ opts.mxc_url = avatarUrl.toStdString();
- connect(proxy.data(),
- &DownloadMediaProxy::avatarDownloaded,
- receiver,
- [user_id, proxy, callback, avatarUrl](const QImage &img) {
- proxy->deleteLater();
- QtConcurrent::run([img, avatarUrl]() {
- QByteArray data;
- QBuffer buffer(&data);
- buffer.open(QIODevice::WriteOnly);
- img.save(&buffer, "PNG");
+ http::v2::client()->get_thumbnail(
+ opts,
+ [opts, proxy = std::move(proxy)](const std::string &res, mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to download avatar: {} - ({} {})",
+ opts.mxc_url,
+ mtx::errors::to_string(err->matrix_error.errcode),
+ err->matrix_error.error);
+ return;
+ }
- cache::client()->saveImage(avatarUrl, data);
- });
- callback(img);
- });
+ cache::client()->saveImage(opts.mxc_url, res);
+
+ auto data = QByteArray(res.data(), res.size());
+ emit proxy->avatarDownloaded(data);
+ });
+}
}
diff --git a/src/Cache.cc b/src/Cache.cc
index c055ab05..397dd05f 100644
--- a/src/Cache.cc
+++ b/src/Cache.cc
@@ -19,7 +19,6 @@
#include <stdexcept>
#include <QByteArray>
-#include <QDebug>
#include <QFile>
#include <QHash>
#include <QStandardPaths>
@@ -27,29 +26,46 @@
#include <variant.hpp>
#include "Cache.h"
+#include "Logging.hpp"
#include "Utils.h"
//! Should be changed when a breaking change occurs in the cache format.
//! This will reset client's data.
-static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.05.11");
+static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.06.10");
+static const std::string SECRET("secret");
static const lmdb::val NEXT_BATCH_KEY("next_batch");
+static const lmdb::val OLM_ACCOUNT_KEY("olm_account");
static const lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version");
//! Cache databases and their format.
//!
//! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc).
//! Format: room_id -> RoomInfo
-static constexpr const char *ROOMS_DB = "rooms";
-static constexpr const char *INVITES_DB = "invites";
+constexpr auto ROOMS_DB("rooms");
+constexpr auto INVITES_DB("invites");
//! Keeps already downloaded media for reuse.
//! Format: matrix_url -> binary data.
-static constexpr const char *MEDIA_DB = "media";
+constexpr auto MEDIA_DB("media");
//! Information that must be kept between sync requests.
-static constexpr const char *SYNC_STATE_DB = "sync_state";
+constexpr auto SYNC_STATE_DB("sync_state");
//! Read receipts per room/event.
-static constexpr const char *READ_RECEIPTS_DB = "read_receipts";
-static constexpr const char *NOTIFICATIONS_DB = "sent_notifications";
+constexpr auto READ_RECEIPTS_DB("read_receipts");
+constexpr auto NOTIFICATIONS_DB("sent_notifications");
+
+//! Encryption related databases.
+
+//! user_id -> list of devices
+constexpr auto DEVICES_DB("devices");
+//! device_id -> device keys
+constexpr auto DEVICE_KEYS_DB("device_keys");
+//! room_ids that have encryption enabled.
+constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms");
+
+//! room_id -> pickled OlmInboundGroupSession
+constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions");
+//! MegolmSessionIndex -> pickled OlmOutboundGroupSession
+constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions");
using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
@@ -62,8 +78,15 @@ namespace cache {
void
init(const QString &user_id)
{
- if (!instance_)
- instance_ = std::make_unique<Cache>(user_id);
+ qRegisterMetaType<SearchResult>();
+ qRegisterMetaType<QVector<SearchResult>>();
+ qRegisterMetaType<RoomMember>();
+ qRegisterMetaType<RoomSearchResult>();
+ qRegisterMetaType<RoomInfo>();
+ qRegisterMetaType<QMap<QString, RoomInfo>>();
+ qRegisterMetaType<std::map<QString, RoomInfo>>();
+
+ instance_ = std::make_unique<Cache>(user_id);
}
Cache *
@@ -71,7 +94,7 @@ client()
{
return instance_.get();
}
-}
+} // namespace cache
Cache::Cache(const QString &userId, QObject *parent)
: QObject{parent}
@@ -82,15 +105,21 @@ Cache::Cache(const QString &userId, QObject *parent)
, mediaDb_{0}
, readReceiptsDb_{0}
, notificationsDb_{0}
+ , devicesDb_{0}
+ , deviceKeysDb_{0}
+ , inboundMegolmSessionDb_{0}
+ , outboundMegolmSessionDb_{0}
, localUserId_{userId}
-{}
+{
+ setup();
+}
void
Cache::setup()
{
- qDebug() << "Setting up cache";
+ nhlog::db()->debug("setting up cache");
- auto statePath = QString("%1/%2/state")
+ auto statePath = QString("%1/%2")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
.arg(QString::fromUtf8(localUserId_.toUtf8().toHex()));
@@ -105,7 +134,7 @@ Cache::setup()
env_.set_max_dbs(1024UL);
if (isInitial) {
- qDebug() << "First time initializing LMDB";
+ nhlog::db()->info("initializing LMDB");
if (!QDir().mkpath(statePath)) {
throw std::runtime_error(
@@ -121,7 +150,7 @@ Cache::setup()
std::string(e.what()));
}
- qWarning() << "Resetting cache due to LMDB version mismatch:" << e.what();
+ nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
QDir stateDir(statePath);
@@ -141,30 +170,315 @@ Cache::setup()
mediaDb_ = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE);
readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
+
+ // Device management
+ devicesDb_ = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE);
+ deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE);
+
+ // Session management
+ inboundMegolmSessionDb_ = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
+ outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
+
txn.commit();
+}
- qRegisterMetaType<RoomInfo>();
+void
+Cache::setEncryptedRoom(const std::string &room_id)
+{
+ nhlog::db()->info("mark room {} as encrypted", room_id);
+
+ auto txn = lmdb::txn::begin(env_);
+ auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
+ lmdb::dbi_put(txn, db, lmdb::val(room_id), lmdb::val("0"));
+ txn.commit();
+}
+
+bool
+Cache::isRoomEncrypted(const std::string &room_id)
+{
+ lmdb::val unused;
+
+ auto txn = lmdb::txn::begin(env_);
+ auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
+ auto res = lmdb::dbi_get(txn, db, lmdb::val(room_id), unused);
+ txn.commit();
+
+ return res;
}
+//
+// Device Management
+//
+
+//
+// Session Management
+//
+
void
-Cache::saveImage(const QString &url, const QByteArray &image)
+Cache::saveInboundMegolmSession(const MegolmSessionIndex &index,
+ mtx::crypto::InboundGroupSessionPtr session)
{
- auto key = url.toUtf8();
+ using namespace mtx::crypto;
+ const auto key = index.to_hash();
+ const auto pickled = pickle<InboundSessionObject>(session.get(), SECRET);
+
+ auto txn = lmdb::txn::begin(env_);
+ lmdb::dbi_put(txn, inboundMegolmSessionDb_, lmdb::val(key), lmdb::val(pickled));
+ txn.commit();
+
+ {
+ std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx);
+ session_storage.group_inbound_sessions[key] = std::move(session);
+ }
+}
+
+OlmInboundGroupSession *
+Cache::getInboundMegolmSession(const MegolmSessionIndex &index)
+{
+ std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx);
+ return session_storage.group_inbound_sessions[index.to_hash()].get();
+}
+
+bool
+Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept
+{
+ std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx);
+ return session_storage.group_inbound_sessions.find(index.to_hash()) !=
+ session_storage.group_inbound_sessions.end();
+}
+
+void
+Cache::updateOutboundMegolmSession(const std::string &room_id, int message_index)
+{
+ using namespace mtx::crypto;
+
+ if (!outboundMegolmSessionExists(room_id))
+ return;
+
+ OutboundGroupSessionData data;
+ OlmOutboundGroupSession *session;
+ {
+ std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
+ data = session_storage.group_outbound_session_data[room_id];
+ session = session_storage.group_outbound_sessions[room_id].get();
+
+ // Update with the current message.
+ data.message_index = message_index;
+ session_storage.group_outbound_session_data[room_id] = data;
+ }
+
+ // Save the updated pickled data for the session.
+ json j;
+ j["data"] = data;
+ j["session"] = pickle<OutboundSessionObject>(session, SECRET);
+
+ auto txn = lmdb::txn::begin(env_);
+ lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump()));
+ txn.commit();
+}
+
+void
+Cache::saveOutboundMegolmSession(const std::string &room_id,
+ const OutboundGroupSessionData &data,
+ mtx::crypto::OutboundGroupSessionPtr session)
+{
+ using namespace mtx::crypto;
+ const auto pickled = pickle<OutboundSessionObject>(session.get(), SECRET);
+
+ json j;
+ j["data"] = data;
+ j["session"] = pickled;
+
+ auto txn = lmdb::txn::begin(env_);
+ lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump()));
+ txn.commit();
+
+ {
+ std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
+ session_storage.group_outbound_session_data[room_id] = data;
+ session_storage.group_outbound_sessions[room_id] = std::move(session);
+ }
+}
+
+bool
+Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept
+{
+ std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
+ return (session_storage.group_outbound_sessions.find(room_id) !=
+ session_storage.group_outbound_sessions.end()) &&
+ (session_storage.group_outbound_session_data.find(room_id) !=
+ session_storage.group_outbound_session_data.end());
+}
+
+OutboundGroupSessionDataRef
+Cache::getOutboundMegolmSession(const std::string &room_id)
+{
+ std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
+ return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[room_id].get(),
+ session_storage.group_outbound_session_data[room_id]};
+}
+
+//
+// OLM sessions.
+//
+
+void
+Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session)
+{
+ using namespace mtx::crypto;
+
+ auto txn = lmdb::txn::begin(env_);
+ auto db = getOlmSessionsDb(txn, curve25519);
+
+ const auto pickled = pickle<SessionObject>(session.get(), SECRET);
+ const auto session_id = mtx::crypto::session_id(session.get());
+
+ lmdb::dbi_put(txn, db, lmdb::val(session_id), lmdb::val(pickled));
+
+ txn.commit();
+}
+
+boost::optional<mtx::crypto::OlmSessionPtr>
+Cache::getOlmSession(const std::string &curve25519, const std::string &session_id)
+{
+ using namespace mtx::crypto;
+
+ auto txn = lmdb::txn::begin(env_);
+ auto db = getOlmSessionsDb(txn, curve25519);
+
+ lmdb::val pickled;
+ bool found = lmdb::dbi_get(txn, db, lmdb::val(session_id), pickled);
+
+ txn.commit();
+
+ if (found) {
+ auto data = std::string(pickled.data(), pickled.size());
+ return unpickle<SessionObject>(data, SECRET);
+ }
+
+ return boost::none;
+}
+
+std::vector<std::string>
+Cache::getOlmSessions(const std::string &curve25519)
+{
+ using namespace mtx::crypto;
+
+ auto txn = lmdb::txn::begin(env_);
+ auto db = getOlmSessionsDb(txn, curve25519);
+
+ std::string session_id, unused;
+ std::vector<std::string> res;
+
+ auto cursor = lmdb::cursor::open(txn, db);
+ while (cursor.get(session_id, unused, MDB_NEXT))
+ res.emplace_back(session_id);
+ cursor.close();
+
+ txn.commit();
+
+ return res;
+}
+
+void
+Cache::saveOlmAccount(const std::string &data)
+{
+ auto txn = lmdb::txn::begin(env_);
+ lmdb::dbi_put(txn, syncStateDb_, OLM_ACCOUNT_KEY, lmdb::val(data));
+ txn.commit();
+}
+
+void
+Cache::restoreSessions()
+{
+ using namespace mtx::crypto;
+
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ std::string key, value;
+
+ //
+ // Inbound Megolm Sessions
+ //
+ {
+ auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_);
+ while (cursor.get(key, value, MDB_NEXT)) {
+ auto session = unpickle<InboundSessionObject>(value, SECRET);
+ session_storage.group_inbound_sessions[key] = std::move(session);
+ }
+ cursor.close();
+ }
+
+ //
+ // Outbound Megolm Sessions
+ //
+ {
+ auto cursor = lmdb::cursor::open(txn, outboundMegolmSessionDb_);
+ while (cursor.get(key, value, MDB_NEXT)) {
+ json obj;
+
+ try {
+ obj = json::parse(value);
+
+ session_storage.group_outbound_session_data[key] =
+ obj.at("data").get<OutboundGroupSessionData>();
+
+ auto session =
+ unpickle<OutboundSessionObject>(obj.at("session"), SECRET);
+ session_storage.group_outbound_sessions[key] = std::move(session);
+ } catch (const nlohmann::json::exception &e) {
+ nhlog::db()->critical(
+ "failed to parse outbound megolm session data: {}", e.what());
+ }
+ }
+ cursor.close();
+ }
+
+ txn.commit();
+
+ nhlog::db()->info("sessions restored");
+}
+
+std::string
+Cache::restoreOlmAccount()
+{
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ lmdb::val pickled;
+ lmdb::dbi_get(txn, syncStateDb_, OLM_ACCOUNT_KEY, pickled);
+ txn.commit();
+
+ return std::string(pickled.data(), pickled.size());
+}
+
+//
+// Media Management
+//
+
+void
+Cache::saveImage(const std::string &url, const std::string &img_data)
+{
+ if (url.empty() || img_data.empty())
+ return;
try {
auto txn = lmdb::txn::begin(env_);
lmdb::dbi_put(txn,
mediaDb_,
- lmdb::val(key.data(), key.size()),
- lmdb::val(image.data(), image.size()));
+ lmdb::val(url.data(), url.size()),
+ lmdb::val(img_data.data(), img_data.size()));
txn.commit();
} catch (const lmdb::error &e) {
- qCritical() << "saveImage:" << e.what();
+ nhlog::db()->critical("saveImage: {}", e.what());
}
}
+void
+Cache::saveImage(const QString &url, const QByteArray &image)
+{
+ saveImage(url.toStdString(), std::string(image.constData(), image.length()));
+}
+
QByteArray
Cache::image(lmdb::txn &txn, const std::string &url) const
{
@@ -180,7 +494,7 @@ Cache::image(lmdb::txn &txn, const std::string &url) const
return QByteArray(image.data(), image.size());
} catch (const lmdb::error &e) {
- qCritical() << "image:" << e.what() << QString::fromStdString(url);
+ nhlog::db()->critical("image: {}, {}", e.what(), url);
}
return QByteArray();
@@ -208,7 +522,7 @@ Cache::image(const QString &url) const
return QByteArray(image.data(), image.size());
} catch (const lmdb::error &e) {
- qCritical() << "image:" << e.what() << url;
+ nhlog::db()->critical("image: {} {}", e.what(), url.toStdString());
}
return QByteArray();
@@ -271,7 +585,7 @@ Cache::isInitialized() const
return res;
}
-QString
+std::string
Cache::nextBatchToken() const
{
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
@@ -281,16 +595,17 @@ Cache::nextBatchToken() const
txn.commit();
- return QString::fromUtf8(token.data(), token.size());
+ return std::string(token.data(), token.size());
}
void
Cache::deleteData()
{
- qInfo() << "Deleting cache data";
-
- if (!cacheDirectory_.isEmpty())
+ // TODO: We need to remove the env_ while not accepting new requests.
+ if (!cacheDirectory_.isEmpty()) {
QDir(cacheDirectory_).removeRecursively();
+ nhlog::db()->info("deleted cache files from disk");
+ }
}
bool
@@ -304,13 +619,14 @@ Cache::isFormatValid()
txn.commit();
if (!res)
- return false;
+ return true;
std::string stored_version(current_version.data(), current_version.size());
if (stored_version != CURRENT_CACHE_FORMAT_VERSION) {
- qWarning() << "Stored format version" << QString::fromStdString(stored_version);
- qWarning() << "There are breaking changes in the cache format.";
+ nhlog::db()->warn("breaking changes in the cache format. stored: {}, current: {}",
+ stored_version,
+ CURRENT_CACHE_FORMAT_VERSION);
return false;
}
@@ -360,7 +676,7 @@ Cache::readReceipts(const QString &event_id, const QString &room_id)
}
} catch (const lmdb::error &e) {
- qCritical() << "readReceipts:" << e.what();
+ nhlog::db()->critical("readReceipts: {}", e.what());
}
return receipts;
@@ -410,7 +726,7 @@ Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Recei
lmdb::val(merged_receipts.data(), merged_receipts.size()));
} catch (const lmdb::error &e) {
- qCritical() << "updateReadReceipts:" << e.what();
+ nhlog::db()->critical("updateReadReceipts: {}", e.what());
}
}
}
@@ -568,9 +884,9 @@ Cache::singleRoomInfo(const std::string &room_id)
return tmp;
} catch (const json::exception &e) {
- qWarning()
- << "failed to parse room info:" << QString::fromStdString(room_id)
- << QString::fromStdString(std::string(data.data(), data.size()));
+ nhlog::db()->warn("failed to parse room info: room_id ({}), {}",
+ room_id,
+ std::string(data.data(), data.size()));
}
}
@@ -584,7 +900,8 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
{
std::map<QString, RoomInfo> room_info;
- auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ // TODO This should be read only.
+ auto txn = lmdb::txn::begin(env_);
for (const auto &room : rooms) {
lmdb::val data;
@@ -600,9 +917,9 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
room_info.emplace(QString::fromStdString(room), std::move(tmp));
} catch (const json::exception &e) {
- qWarning()
- << "failed to parse room info:" << QString::fromStdString(room)
- << QString::fromStdString(std::string(data.data(), data.size()));
+ nhlog::db()->warn("failed to parse room info: room_id ({}), {}",
+ room,
+ std::string(data.data(), data.size()));
}
} else {
// Check if the room is an invite.
@@ -615,10 +932,10 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
room_info.emplace(QString::fromStdString(room),
std::move(tmp));
} catch (const json::exception &e) {
- qWarning() << "failed to parse room info for invite:"
- << QString::fromStdString(room)
- << QString::fromStdString(
- std::string(data.data(), data.size()));
+ nhlog::db()->warn(
+ "failed to parse room info for invite: room_id ({}), {}",
+ room,
+ std::string(data.data(), data.size()));
}
}
}
@@ -703,7 +1020,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn,
return QString::fromStdString(msg.content.url);
} catch (const json::exception &e) {
- qWarning() << QString::fromStdString(e.what());
+ nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
}
}
@@ -726,7 +1043,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn,
cursor.close();
return QString::fromStdString(m.avatar_url);
} catch (const json::exception &e) {
- qWarning() << QString::fromStdString(e.what());
+ nhlog::db()->warn("failed to parse member info: {}", e.what());
}
}
@@ -753,7 +1070,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
if (!msg.content.name.empty())
return QString::fromStdString(msg.content.name);
} catch (const json::exception &e) {
- qWarning() << QString::fromStdString(e.what());
+ nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
}
}
@@ -768,7 +1085,8 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
if (!msg.content.alias.empty())
return QString::fromStdString(msg.content.alias);
} catch (const json::exception &e) {
- qWarning() << QString::fromStdString(e.what());
+ nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}",
+ e.what());
}
}
@@ -784,7 +1102,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
try {
members.emplace(user_id, json::parse(member_data));
} catch (const json::exception &e) {
- qWarning() << QString::fromStdString(e.what());
+ nhlog::db()->warn("failed to parse member info: {}", e.what());
}
ii++;
@@ -828,7 +1146,7 @@ Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb)
json::parse(std::string(event.data(), event.size()));
return msg.content.join_rule;
} catch (const json::exception &e) {
- qWarning() << e.what();
+ nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what());
}
}
return JoinRule::Knock;
@@ -850,7 +1168,8 @@ Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb)
json::parse(std::string(event.data(), event.size()));
return msg.content.guest_access == AccessState::CanJoin;
} catch (const json::exception &e) {
- qWarning() << e.what();
+ nhlog::db()->warn("failed to parse m.room.guest_access event: {}",
+ e.what());
}
}
return false;
@@ -874,7 +1193,7 @@ Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb)
if (!msg.content.topic.empty())
return QString::fromStdString(msg.content.topic);
} catch (const json::exception &e) {
- qWarning() << QString::fromStdString(e.what());
+ nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
}
}
@@ -897,7 +1216,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members
json::parse(std::string(event.data(), event.size()));
return QString::fromStdString(msg.content.name);
} catch (const json::exception &e) {
- qWarning() << QString::fromStdString(e.what());
+ nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
}
}
@@ -914,7 +1233,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members
return QString::fromStdString(tmp.name);
} catch (const json::exception &e) {
- qWarning() << QString::fromStdString(e.what());
+ nhlog::db()->warn("failed to parse member info: {}", e.what());
}
}
@@ -939,7 +1258,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me
json::parse(std::string(event.data(), event.size()));
return QString::fromStdString(msg.content.url);
} catch (const json::exception &e) {
- qWarning() << QString::fromStdString(e.what());
+ nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
}
}
@@ -956,7 +1275,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me
return QString::fromStdString(tmp.avatar_url);
} catch (const json::exception &e) {
- qWarning() << QString::fromStdString(e.what());
+ nhlog::db()->warn("failed to parse member info: {}", e.what());
}
}
@@ -981,7 +1300,7 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
json::parse(std::string(event.data(), event.size()));
return QString::fromStdString(msg.content.topic);
} catch (const json::exception &e) {
- qWarning() << QString::fromStdString(e.what());
+ nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
}
}
@@ -1017,8 +1336,9 @@ Cache::getRoomAvatar(const std::string &room_id)
return QImage();
}
} catch (const json::exception &e) {
- qWarning() << "failed to parse room info" << e.what()
- << QString::fromStdString(std::string(response.data(), response.size()));
+ nhlog::db()->warn("failed to parse room info: {}, {}",
+ e.what(),
+ std::string(response.data(), response.size()));
}
if (!lmdb::dbi_get(txn, mediaDb_, lmdb::val(media_url), response)) {
@@ -1054,7 +1374,7 @@ void
Cache::populateMembers()
{
auto rooms = joinedRooms();
- qDebug() << "loading" << rooms.size() << "rooms";
+ nhlog::db()->info("loading {} rooms", rooms.size());
auto txn = lmdb::txn::begin(env_);
@@ -1182,7 +1502,7 @@ Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_
QString::fromStdString(tmp.name),
QImage::fromData(image(txn, tmp.avatar_url))});
} catch (const json::exception &e) {
- qWarning() << e.what();
+ nhlog::db()->warn("{}", e.what());
}
currentIndex += 1;
@@ -1253,7 +1573,8 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes
std::min(min_event_level,
(uint16_t)msg.content.state_level(to_string(ty)));
} catch (const json::exception &e) {
- qWarning() << "hasEnoughPowerLevel: " << e.what();
+ nhlog::db()->warn("failed to parse m.room.power_levels event: {}",
+ e.what());
}
}
@@ -1262,6 +1583,26 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes
return user_level >= min_event_level;
}
+std::vector<std::string>
+Cache::roomMembers(const std::string &room_id)
+{
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+
+ std::vector<std::string> members;
+ std::string user_id, unused;
+
+ auto db = getMembersDb(txn, room_id);
+
+ auto cursor = lmdb::cursor::open(txn, db);
+ while (cursor.get(user_id, unused, MDB_NEXT))
+ members.emplace_back(std::move(user_id));
+ cursor.close();
+
+ txn.commit();
+
+ return members;
+}
+
QHash<QString, QString> Cache::DisplayNames;
QHash<QString, QString> Cache::AvatarUrls;
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index 9ae860fb..e543cdf9 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -16,15 +16,16 @@
*/
#include <QApplication>
-#include <QDebug>
#include <QSettings>
#include <QtConcurrent>
#include "AvatarProvider.h"
#include "Cache.h"
#include "ChatPage.h"
+#include "Logging.hpp"
#include "MainWindow.h"
#include "MatrixClient.h"
+#include "Olm.hpp"
#include "OverlayModal.h"
#include "QuickSwitcher.h"
#include "RoomList.h"
@@ -43,13 +44,16 @@
#include "dialogs/ReadReceipts.h"
#include "timeline/TimelineViewManager.h"
-constexpr int SYNC_RETRY_TIMEOUT = 40 * 1000;
-constexpr int INITIAL_SYNC_RETRY_TIMEOUT = 240 * 1000;
+// TODO: Needs to be updated with an actual secret.
+static const std::string STORAGE_SECRET_KEY("secret");
-ChatPage *ChatPage::instance_ = nullptr;
+ChatPage *ChatPage::instance_ = nullptr;
+constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
+constexpr size_t MAX_ONETIME_KEYS = 50;
ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
: QWidget(parent)
+ , isConnected_(true)
, userSettings_{userSettings}
{
setObjectName("chatPage");
@@ -78,13 +82,12 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
sidebarActions_ = new SideBarActions(this);
connect(
sidebarActions_, &SideBarActions::showSettings, this, &ChatPage::showUserSettingsPage);
- connect(
- sidebarActions_, &SideBarActions::joinRoom, http::client(), &MatrixClient::joinRoom);
- connect(
- sidebarActions_, &SideBarActions::createRoom, http::client(), &MatrixClient::createRoom);
+ connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom);
+ connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom);
user_info_widget_ = new UserInfoWidget(sideBar_);
room_list_ = new RoomList(userSettings_, sideBar_);
+ connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom);
sideBarLayout_->addWidget(user_info_widget_);
sideBarLayout_->addWidget(room_list_);
@@ -107,6 +110,11 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
contentLayout_->addWidget(top_bar_);
contentLayout_->addWidget(view_manager_);
+ connect(this,
+ &ChatPage::removeTimelineEvent,
+ view_manager_,
+ &TimelineViewManager::removeTimelineEvent);
+
// Splitter
splitter->addWidget(sideBar_);
splitter->addWidget(content_);
@@ -120,16 +128,82 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
typingRefresher_ = new QTimer(this);
typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT);
+ connect(this, &ChatPage::connectionLost, this, [this]() {
+ nhlog::net()->info("connectivity lost");
+ isConnected_ = false;
+ http::v2::client()->shutdown();
+ text_input_->disableInput();
+ });
+ connect(this, &ChatPage::connectionRestored, this, [this]() {
+ nhlog::net()->info("trying to re-connect");
+ text_input_->enableInput();
+ isConnected_ = true;
+
+ // Drop all pending connections.
+ http::v2::client()->shutdown();
+ trySync();
+ });
+
+ connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
+ connect(&connectivityTimer_, &QTimer::timeout, this, [=]() {
+ if (http::v2::client()->access_token().empty()) {
+ connectivityTimer_.stop();
+ return;
+ }
+
+ http::v2::client()->versions(
+ [this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
+ if (err) {
+ emit connectionLost();
+ return;
+ }
+
+ if (!isConnected_)
+ emit connectionRestored();
+ });
+ });
+
+ connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
connect(user_info_widget_, &UserInfoWidget::logout, this, [this]() {
- http::client()->logout();
+ http::v2::client()->logout(
+ [this](const mtx::responses::Logout &, mtx::http::RequestErr err) {
+ if (err) {
+ // TODO: handle special errors
+ emit contentLoaded();
+ nhlog::net()->warn(
+ "failed to logout: {} - {}",
+ mtx::errors::to_string(err->matrix_error.errcode),
+ err->matrix_error.error);
+ return;
+ }
+
+ emit loggedOut();
+ });
+
emit showOverlayProgressBar();
});
- connect(http::client(), &MatrixClient::loggedOut, this, &ChatPage::logout);
connect(top_bar_, &TopRoomBar::inviteUsers, this, [this](QStringList users) {
+ const auto room_id = current_room_.toStdString();
+
for (int ii = 0; ii < users.size(); ++ii) {
- QTimer::singleShot(ii * 1000, this, [this, ii, users]() {
- http::client()->inviteUser(current_room_, users.at(ii));
+ QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() {
+ const auto user = users.at(ii);
+
+ http::v2::client()->invite_user(
+ room_id,
+ user.toStdString(),
+ [this, user](const mtx::responses::RoomInvite &,
+ mtx::http::RequestErr err) {
+ if (err) {
+ emit showNotification(
+ QString("Failed to invite user: %1").arg(user));
+ return;
+ }
+
+ emit showNotification(
+ QString("Invited user: %1").arg(user));
+ });
});
}
});
@@ -155,36 +229,34 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) {
view_manager_->addRoom(room_id);
- http::client()->joinRoom(room_id);
+ joinRoom(room_id);
room_list_->removeRoom(room_id, currentRoom() == room_id);
});
connect(room_list_, &RoomList::declineInvite, this, [this](const QString &room_id) {
- http::client()->leaveRoom(room_id);
+ leaveRoom(room_id);
room_list_->removeRoom(room_id, currentRoom() == room_id);
});
- connect(text_input_, &TextInputWidget::startedTyping, this, [this]() {
- if (!userSettings_->isTypingNotificationsEnabled())
- return;
-
- typingRefresher_->start();
- http::client()->sendTypingNotification(current_room_);
- });
-
+ connect(
+ text_input_, &TextInputWidget::startedTyping, this, &ChatPage::sendTypingNotifications);
+ connect(typingRefresher_, &QTimer::timeout, this, &ChatPage::sendTypingNotifications);
connect(text_input_, &TextInputWidget::stoppedTyping, this, [this]() {
if (!userSettings_->isTypingNotificationsEnabled())
return;
typingRefresher_->stop();
- http::client()->removeTypingNotification(current_room_);
- });
- connect(typingRefresher_, &QTimer::timeout, this, [this]() {
- if (!userSettings_->isTypingNotificationsEnabled())
+ if (current_room_.isEmpty())
return;
- http::client()->sendTypingNotification(current_room_);
+ http::v2::client()->stop_typing(
+ current_room_.toStdString(), [](mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to stop typing notifications: {}",
+ err->matrix_error.error);
+ }
+ });
});
connect(view_manager_,
@@ -207,142 +279,242 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
view_manager_,
SLOT(queueEmoteMessage(const QString &)));
- connect(text_input_,
- &TextInputWidget::sendJoinRoomRequest,
- http::client(),
- &MatrixClient::joinRoom);
+ connect(text_input_, &TextInputWidget::sendJoinRoomRequest, this, &ChatPage::joinRoom);
connect(text_input_,
&TextInputWidget::uploadImage,
this,
- [this](QSharedPointer<QIODevice> data, const QString &fn) {
- http::client()->uploadImage(current_room_, fn, data);
+ [this](QSharedPointer<QIODevice> dev, const QString &fn) {
+ QMimeDatabase db;
+ QMimeType mime = db.mimeTypeForData(dev.data());
+
+ if (!dev->open(QIODevice::ReadOnly)) {
+ emit uploadFailed(
+ QString("Error while reading media: %1").arg(dev->errorString()));
+ return;
+ }
+
+ auto bin = dev->readAll();
+ auto payload = std::string(bin.data(), bin.size());
+
+ http::v2::client()->upload(
+ payload,
+ mime.name().toStdString(),
+ QFileInfo(fn).fileName().toStdString(),
+ [this,
+ room_id = current_room_,
+ filename = fn,
+ mime = mime.name(),
+ size = payload.size()](const mtx::responses::ContentURI &res,
+ mtx::http::RequestErr err) {
+ if (err) {
+ emit uploadFailed(
+ tr("Failed to upload image. Please try again."));
+ nhlog::net()->warn("failed to upload image: {} ({})",
+ err->matrix_error.error,
+ static_cast<int>(err->status_code));
+ return;
+ }
+
+ emit imageUploaded(room_id,
+ filename,
+ QString::fromStdString(res.content_uri),
+ mime,
+ size);
+ });
});
connect(text_input_,
&TextInputWidget::uploadFile,
this,
- [this](QSharedPointer<QIODevice> data, const QString &fn) {
- http::client()->uploadFile(current_room_, fn, data);
+ [this](QSharedPointer<QIODevice> dev, const QString &fn) {
+ QMimeDatabase db;
+ QMimeType mime = db.mimeTypeForData(dev.data());
+
+ if (!dev->open(QIODevice::ReadOnly)) {
+ emit uploadFailed(
+ QString("Error while reading media: %1").arg(dev->errorString()));
+ return;
+ }
+
+ auto bin = dev->readAll();
+ auto payload = std::string(bin.data(), bin.size());
+
+ http::v2::client()->upload(
+ payload,
+ mime.name().toStdString(),
+ QFileInfo(fn).fileName().toStdString(),
+ [this,
+ room_id = current_room_,
+ filename = fn,
+ mime = mime.name(),
+ size = payload.size()](const mtx::responses::ContentURI &res,
+ mtx::http::RequestErr err) {
+ if (err) {
+ emit uploadFailed(
+ tr("Failed to upload file. Please try again."));
+ nhlog::net()->warn("failed to upload file: {} ({})",
+ err->matrix_error.error,
+ static_cast<int>(err->status_code));
+ return;
+ }
+
+ emit fileUploaded(room_id,
+ filename,
+ QString::fromStdString(res.content_uri),
+ mime,
+ size);
+ });
});
connect(text_input_,
&TextInputWidget::uploadAudio,
this,
- [this](QSharedPointer<QIODevice> data, const QString &fn) {
- http::client()->uploadAudio(current_room_, fn, data);
+ [this](QSharedPointer<QIODevice> dev, const QString &fn) {
+ QMimeDatabase db;
+ QMimeType mime = db.mimeTypeForData(dev.data());
+
+ if (!dev->open(QIODevice::ReadOnly)) {
+ emit uploadFailed(
+ QString("Error while reading media: %1").arg(dev->errorString()));
+ return;
+ }
+
+ auto bin = dev->readAll();
+ auto payload = std::string(bin.data(), bin.size());
+
+ http::v2::client()->upload(
+ payload,
+ mime.name().toStdString(),
+ QFileInfo(fn).fileName().toStdString(),
+ [this,
+ room_id = current_room_,
+ filename = fn,
+ mime = mime.name(),
+ size = payload.size()](const mtx::responses::ContentURI &res,
+ mtx::http::RequestErr err) {
+ if (err) {
+ emit uploadFailed(
+ tr("Failed to upload audio. Please try again."));
+ nhlog::net()->warn("failed to upload audio: {} ({})",
+ err->matrix_error.error,
+ static_cast<int>(err->status_code));
+ return;
+ }
+
+ emit audioUploaded(room_id,
+ filename,
+ QString::fromStdString(res.content_uri),
+ mime,
+ size);
+ });
});
connect(text_input_,
&TextInputWidget::uploadVideo,
this,
- [this](QSharedPointer<QIODevice> data, const QString &fn) {
- http::client()->uploadVideo(current_room_, fn, data);
+ [this](QSharedPointer<QIODevice> dev, const QString &fn) {
+ QMimeDatabase db;
+ QMimeType mime = db.mimeTypeForData(dev.data());
+
+ if (!dev->open(QIODevice::ReadOnly)) {
+ emit uploadFailed(
+ QString("Error while reading media: %1").arg(dev->errorString()));
+ return;
+ }
+
+ auto bin = dev->readAll();
+ auto payload = std::string(bin.data(), bin.size());
+
+ http::v2::client()->upload(
+ payload,
+ mime.name().toStdString(),
+ QFileInfo(fn).fileName().toStdString(),
+ [this,
+ room_id = current_room_,
+ filename = fn,
+ mime = mime.name(),
+ size = payload.size()](const mtx::responses::ContentURI &res,
+ mtx::http::RequestErr err) {
+ if (err) {
+ emit uploadFailed(
+ tr("Failed to upload video. Please try again."));
+ nhlog::net()->warn("failed to upload video: {} ({})",
+ err->matrix_error.error,
+ static_cast<int>(err->status_code));
+ return;
+ }
+
+ emit videoUploaded(room_id,
+ filename,
+ QString::fromStdString(res.content_uri),
+ mime,
+ size);
+ });
});
- connect(
- http::client(), &MatrixClient::roomCreationFailed, this, &ChatPage::showNotification);
- connect(http::client(), &MatrixClient::joinFailed, this, &ChatPage::showNotification);
- connect(http::client(), &MatrixClient::uploadFailed, this, [this](int, const QString &msg) {
+ connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) {
text_input_->hideUploadSpinner();
emit showNotification(msg);
});
- connect(
- http::client(),
- &MatrixClient::imageUploaded,
- this,
- [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) {
- text_input_->hideUploadSpinner();
- view_manager_->queueImageMessage(roomid, filename, url, mime, dsize);
- });
- connect(
- http::client(),
- &MatrixClient::fileUploaded,
- this,
- [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) {
- text_input_->hideUploadSpinner();
- view_manager_->queueFileMessage(roomid, filename, url, mime, dsize);
- });
- connect(
- http::client(),
- &MatrixClient::audioUploaded,
- this,
- [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) {
- text_input_->hideUploadSpinner();
- view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize);
- });
- connect(
- http::client(),
- &MatrixClient::videoUploaded,
- this,
- [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) {
- text_input_->hideUploadSpinner();
- view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize);
- });
-
- connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar);
-
- connect(http::client(),
- &MatrixClient::initialSyncCompleted,
- this,
- &ChatPage::initialSyncCompleted);
- connect(
- http::client(), &MatrixClient::initialSyncFailed, this, &ChatPage::retryInitialSync);
- connect(http::client(), &MatrixClient::syncCompleted, this, &ChatPage::syncCompleted);
- connect(http::client(),
- &MatrixClient::getOwnProfileResponse,
+ connect(this,
+ &ChatPage::imageUploaded,
this,
- &ChatPage::updateOwnProfileInfo);
- connect(http::client(),
- SIGNAL(getOwnCommunitiesResponse(QList<QString>)),
+ [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) {
+ text_input_->hideUploadSpinner();
+ view_manager_->queueImageMessage(roomid, filename, url, mime, dsize);
+ });
+ connect(this,
+ &ChatPage::fileUploaded,
this,
- SLOT(updateOwnCommunitiesInfo(QList<QString>)));
- connect(http::client(),
- &MatrixClient::communityProfileRetrieved,
+ [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) {
+ text_input_->hideUploadSpinner();
+ view_manager_->queueFileMessage(roomid, filename, url, mime, dsize);
+ });
+ connect(this,
+ &ChatPage::audioUploaded,
this,
- [this](QString communityId, QJsonObject profile) {
- communities_[communityId]->parseProfile(profile);
+ [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) {
+ text_input_->hideUploadSpinner();
+ view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize);
});
- connect(http::client(),
- &MatrixClient::communityRoomsRetrieved,
+ connect(this,
+ &ChatPage::videoUploaded,
this,
- [this](QString communityId, QJsonObject rooms) {
- communities_[communityId]->parseRooms(rooms);
-
- if (communityId == current_community_) {
- if (communityId == "world") {
- room_list_->setFilterRooms(false);
- } else {
- room_list_->setRoomFilter(
- communities_[communityId]->getRoomList());
- }
- }
+ [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) {
+ text_input_->hideUploadSpinner();
+ view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize);
});
- connect(http::client(), &MatrixClient::joinedRoom, this, [this](const QString &room_id) {
- emit showNotification("You joined the room.");
+ connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar);
- // We remove any invites with the same room_id.
- try {
- cache::client()->removeInvite(room_id.toStdString());
- } catch (const lmdb::error &e) {
- emit showNotification(QString("Failed to remove invite: %1")
- .arg(QString::fromStdString(e.what())));
- }
- });
- connect(http::client(), &MatrixClient::leftRoom, this, &ChatPage::removeRoom);
- connect(http::client(), &MatrixClient::invitedUser, this, [this](QString, QString user) {
- emit showNotification(QString("Invited user %1").arg(user));
- });
- connect(http::client(), &MatrixClient::roomCreated, this, [this](QString room_id) {
- emit showNotification(QString("Room %1 created").arg(room_id));
- });
- connect(http::client(), &MatrixClient::redactionFailed, this, [this](const QString &error) {
- emit showNotification(QString("Message redaction failed: %1").arg(error));
- });
- connect(http::client(),
- &MatrixClient::notificationsRetrieved,
- this,
- &ChatPage::sendDesktopNotifications);
+ // connect(http::client(),
+ // SIGNAL(getOwnCommunitiesResponse(QList<QString>)),
+ // this,
+ // SLOT(updateOwnCommunitiesInfo(QList<QString>)));
+ // connect(http::client(),
+ // &MatrixClient::communityProfileRetrieved,
+ // this,
+ // [this](QString communityId, QJsonObject profile) {
+ // communities_[communityId]->parseProfile(profile);
+ // });
+ // connect(http::client(),
+ // &MatrixClient::communityRoomsRetrieved,
+ // this,
+ // [this](QString communityId, QJsonObject rooms) {
+ // communities_[communityId]->parseRooms(rooms);
+
+ // if (communityId == current_community_) {
+ // if (communityId == "world") {
+ // room_list_->setFilterRooms(false);
+ // } else {
+ // room_list_->setRoomFilter(
+ // communities_[communityId]->getRoomList());
+ // }
+ // }
+ // });
+
+ connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
+ connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendDesktopNotifications);
showContentTimer_ = new QTimer(this);
showContentTimer_->setSingleShot(true);
@@ -361,20 +533,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
}
});
- initialSyncTimer_ = new QTimer(this);
- connect(initialSyncTimer_, &QTimer::timeout, this, [this]() { retryInitialSync(); });
-
- syncTimeoutTimer_ = new QTimer(this);
- connect(syncTimeoutTimer_, &QTimer::timeout, this, [this]() {
- if (http::client()->getHomeServer().isEmpty()) {
- syncTimeoutTimer_->stop();
- return;
- }
-
- qDebug() << "Sync took too long. Retrying...";
- http::client()->sync();
- });
-
connect(communitiesList_,
&CommunitiesList::communityChanged,
this,
@@ -394,12 +552,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
this,
&ChatPage::setGroupViewState);
- connect(this, &ChatPage::continueSync, this, [this](const QString &next_batch) {
- syncTimeoutTimer_->start(SYNC_RETRY_TIMEOUT);
- http::client()->setNextBatchToken(next_batch);
- http::client()->sync();
- });
-
connect(this, &ChatPage::startConsesusTimer, this, [this]() {
consensusTimer_->start(CONSENSUS_TIMEOUT);
showContentTimer_->start(SHOW_CONTENT_TIMEOUT);
@@ -418,7 +570,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
try {
room_list_->cleanupInvites(cache::client()->invites());
} catch (const lmdb::error &e) {
- qWarning() << "failed to retrieve invites" << e.what();
+ nhlog::db()->error("failed to retrieve invites: {}", e.what());
}
view_manager_->initialize(rooms);
@@ -437,7 +589,20 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
}
if (hasNotifications)
- http::client()->getNotifications();
+ http::v2::client()->notifications(
+ 5,
+ [this](const mtx::responses::Notifications &res,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn(
+ "failed to retrieve notifications: {} ({})",
+ err->matrix_error.error,
+ static_cast<int>(err->status_code));
+ return;
+ }
+
+ emit notificationsRetrieved(std::move(res));
+ });
});
connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync);
connect(
@@ -446,12 +611,24 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
changeTopRoomInfo(currentRoom());
});
- instance_ = this;
+ // Callbacks to update the user info (top left corner of the page).
+ connect(this, &ChatPage::setUserAvatar, user_info_widget_, &UserInfoWidget::setAvatar);
+ connect(this, &ChatPage::setUserDisplayName, this, [this](const QString &name) {
+ QSettings settings;
+ auto userid = settings.value("auth/user_id").toString();
+ user_info_widget_->setUserId(userid);
+ user_info_widget_->setDisplayName(name);
+ });
+
+ connect(this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync);
+ connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync);
+ connect(this, &ChatPage::tryDelayedSyncCb, this, [this]() {
+ QTimer::singleShot(5000, this, &ChatPage::trySync);
+ });
+
+ connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
- qRegisterMetaType<std::map<QString, RoomInfo>>();
- qRegisterMetaType<QMap<QString, RoomInfo>>();
- qRegisterMetaType<mtx::responses::Rooms>();
- qRegisterMetaType<std::vector<std::string>>();
+ instance_ = this;
}
void
@@ -462,6 +639,19 @@ ChatPage::logout()
resetUI();
emit closing();
+ connectivityTimer_.stop();
+}
+
+void
+ChatPage::dropToLoginPage(const QString &msg)
+{
+ deleteConfigs();
+ resetUI();
+
+ http::v2::client()->shutdown();
+ connectivityTimer_.stop();
+
+ emit showLoginPage(msg);
}
void
@@ -490,90 +680,71 @@ ChatPage::deleteConfigs()
settings.endGroup();
cache::client()->deleteData();
-
- http::client()->reset();
+ http::v2::client()->clear();
}
void
ChatPage::bootstrap(QString userid, QString homeserver, QString token)
{
- http::client()->setServer(homeserver);
- http::client()->setAccessToken(token);
- http::client()->getOwnProfile();
- http::client()->getOwnCommunities();
+ using namespace mtx::identifiers;
+
+ try {
+ http::v2::client()->set_user(parse<User>(userid.toStdString()));
+ } catch (const std::invalid_argument &e) {
+ nhlog::ui()->critical("bootstrapped with invalid user_id: {}",
+ userid.toStdString());
+ }
+
+ http::v2::client()->set_server(homeserver.toStdString());
+ http::v2::client()->set_access_token(token.toStdString());
- cache::init(userid);
+ // The Olm client needs the user_id & device_id that will be included
+ // in the generated payloads & keys.
+ olm::client()->set_user_id(http::v2::client()->user_id().to_string());
+ olm::client()->set_device_id(http::v2::client()->device_id());
try {
- cache::client()->setup();
+ cache::init(userid);
- if (!cache::client()->isFormatValid()) {
+ const bool isInitialized = cache::client()->isInitialized();
+ const bool isValid = cache::client()->isFormatValid();
+
+ if (isInitialized && !isValid) {
+ nhlog::db()->warn("breaking changes in cache");
+ // TODO: Deleting session data but keep using the
+ // same device doesn't work.
cache::client()->deleteData();
- cache::client()->setup();
- cache::client()->setCurrentFormat();
- }
- if (cache::client()->isInitialized()) {
+ cache::init(userid);
+ cache::client()->setCurrentFormat();
+ } else if (isInitialized) {
loadStateFromCache();
return;
}
} catch (const lmdb::error &e) {
- qCritical() << "Cache failure" << e.what();
+ nhlog::db()->critical("failure during boot: {}", e.what());
cache::client()->deleteData();
- qInfo() << "Falling back to initial sync ...";
+ nhlog::net()->info("falling back to initial sync");
}
- http::client()->initialSync();
-
- initialSyncTimer_->start(INITIAL_SYNC_RETRY_TIMEOUT);
-}
-
-void
-ChatPage::syncCompleted(const mtx::responses::Sync &response)
-{
- syncTimeoutTimer_->stop();
-
- QtConcurrent::run([this, res = std::move(response)]() {
- try {
- cache::client()->saveState(res);
- emit syncUI(res.rooms);
-
- auto updates = cache::client()->roomUpdates(res);
-
- emit syncTopBar(updates);
- emit syncRoomlist(updates);
-
- } catch (const lmdb::error &e) {
- std::cout << "save cache error:" << e.what() << '\n';
- // TODO: retry sync.
- return;
- }
-
- emit continueSync(cache::client()->nextBatchToken());
- });
-}
-
-void
-ChatPage::initialSyncCompleted(const mtx::responses::Sync &response)
-{
- initialSyncTimer_->stop();
-
- qDebug() << "initial sync completed";
-
- QtConcurrent::run([this, res = std::move(response)]() {
- try {
- cache::client()->saveState(res);
- emit initializeViews(std::move(res.rooms));
- emit initializeRoomList(cache::client()->roomInfo());
- } catch (const lmdb::error &e) {
- qWarning() << "cache error:" << QString::fromStdString(e.what());
- emit retryInitialSync();
- return;
- }
+ try {
+ // 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::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY));
+ } 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;
+ }
- emit continueSync(cache::client()->nextBatchToken());
- emit contentLoaded();
- });
+ getProfileInfo();
+ tryInitialSync();
}
void
@@ -586,41 +757,6 @@ ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img)
}
void
-ChatPage::updateOwnProfileInfo(const QUrl &avatar_url, const QString &display_name)
-{
- QSettings settings;
- auto userid = settings.value("auth/user_id").toString();
-
- user_info_widget_->setUserId(userid);
- user_info_widget_->setDisplayName(display_name);
-
- if (!avatar_url.isValid())
- return;
-
- if (cache::client()) {
- auto data = cache::client()->image(avatar_url.toString());
- if (!data.isNull()) {
- user_info_widget_->setAvatar(QImage::fromData(data));
- return;
- }
- }
-
- auto proxy = http::client()->fetchUserAvatar(avatar_url);
-
- if (proxy.isNull())
- return;
-
- proxy->setParent(this);
- connect(proxy.data(),
- &DownloadMediaProxy::avatarDownloaded,
- this,
- [this, proxy](const QImage &img) {
- proxy->deleteLater();
- user_info_widget_->setAvatar(img);
- });
-}
-
-void
ChatPage::updateOwnCommunitiesInfo(const QList<QString> &own_communities)
{
for (int i = 0; i < own_communities.size(); i++) {
@@ -636,7 +772,7 @@ void
ChatPage::changeTopRoomInfo(const QString &room_id)
{
if (room_id.isEmpty()) {
- qWarning() << "can't switch to empty room_id";
+ nhlog::ui()->warn("cannot switch to empty room_id");
return;
}
@@ -660,7 +796,7 @@ ChatPage::changeTopRoomInfo(const QString &room_id)
top_bar_->updateRoomAvatar(img);
} catch (const lmdb::error &e) {
- qWarning() << "failed to change top bar room info" << e.what();
+ nhlog::ui()->error("failed to change top bar room info: {}", e.what());
}
current_room_ = room_id;
@@ -681,22 +817,37 @@ ChatPage::showUnreadMessageNotification(int count)
void
ChatPage::loadStateFromCache()
{
- qDebug() << "restoring state from cache";
+ nhlog::db()->info("restoring state from cache");
+
+ getProfileInfo();
QtConcurrent::run([this]() {
try {
+ cache::client()->restoreSessions();
+ olm::client()->load(cache::client()->restoreOlmAccount(),
+ STORAGE_SECRET_KEY);
+
cache::client()->populateMembers();
emit initializeEmptyViews(cache::client()->joinedRooms());
emit initializeRoomList(cache::client()->roomInfo());
+ } 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) {
- std::cout << "load cache error:" << e.what() << '\n';
- // TODO Clear cache and restart.
+ nhlog::db()->critical("failed to restore cache: {}", 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);
+
// Start receiving events.
- emit continueSync(cache::client()->nextBatchToken());
+ emit trySyncCb();
// Check periodically if the timelines have been loaded.
emit startConsesusTimer();
@@ -740,7 +891,7 @@ ChatPage::removeRoom(const QString &room_id)
cache::client()->removeRoom(room_id);
cache::client()->removeInvite(room_id.toStdString());
} catch (const lmdb::error &e) {
- qCritical() << "The cache couldn't be updated: " << e.what();
+ nhlog::db()->critical("failure while removing room: {}", e.what());
// TODO: Notify the user.
}
@@ -824,33 +975,6 @@ ChatPage::setGroupViewState(bool isEnabled)
}
void
-ChatPage::retryInitialSync(int status_code)
-{
- initialSyncTimer_->stop();
-
- if (http::client()->getHomeServer().isEmpty()) {
- deleteConfigs();
- resetUI();
- emit showLoginPage("Sync error. Please try again.");
- return;
- }
-
- // Retry on Bad-Gateway & Gateway-Timeout errors
- if (status_code == -1 || status_code == 504 || status_code == 502 || status_code == 524) {
- qWarning() << "retrying initial sync";
-
- http::client()->initialSync();
- initialSyncTimer_->start(INITIAL_SYNC_RETRY_TIMEOUT);
- } else {
- // Drop into the login screen.
- deleteConfigs();
- resetUI();
-
- emit showLoginPage(QString("Sync error %1. Please try again.").arg(status_code));
- }
-}
-
-void
ChatPage::updateRoomNotificationCount(const QString &room_id, uint16_t notification_count)
{
room_list_->updateUnreadMessageCount(room_id, notification_count);
@@ -886,7 +1010,321 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res)
utils::event_body(item.event));
}
} catch (const lmdb::error &e) {
- qWarning() << e.what();
+ nhlog::db()->warn("error while sending desktop notification: {}", e.what());
}
}
}
+
+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);
+
+ http::v2::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);
+ nhlog::crypto()->critical("failed to upload one time keys: {} {}",
+ err->matrix_error.error,
+ status_code);
+ // TODO We should have a timeout instead of keeping hammering the server.
+ emit tryInitialSyncCb();
+ return;
+ }
+
+ olm::client()->mark_keys_as_published();
+ for (const auto &entry : res.one_time_key_counts)
+ nhlog::net()->info(
+ "uploaded {} {} one-time keys", entry.second, entry.first);
+
+ nhlog::net()->info("trying initial sync");
+
+ mtx::http::SyncOpts opts;
+ opts.timeout = 0;
+ http::v2::client()->sync(opts,
+ std::bind(&ChatPage::initialSyncHandler,
+ this,
+ std::placeholders::_1,
+ std::placeholders::_2));
+ });
+}
+
+void
+ChatPage::trySync()
+{
+ mtx::http::SyncOpts opts;
+
+ if (!connectivityTimer_.isActive())
+ connectivityTimer_.start();
+
+ try {
+ opts.since = cache::client()->nextBatchToken();
+ } catch (const lmdb::error &e) {
+ nhlog::db()->error("failed to retrieve next batch token: {}", e.what());
+ return;
+ }
+
+ http::v2::client()->sync(
+ opts, [this](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);
+ 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("sync error: {} {}", status_code, err_code);
+
+ if (status_code <= 0 || status_code >= 600) {
+ if (!http::v2::is_logged_in())
+ return;
+
+ emit tryDelayedSyncCb();
+ return;
+ }
+
+ switch (status_code) {
+ case 502:
+ case 504:
+ case 524: {
+ emit trySyncCb();
+ return;
+ }
+ default: {
+ if (!http::v2::is_logged_in())
+ return;
+
+ if (err->matrix_error.errcode ==
+ mtx::errors::ErrorCode::M_UNKNOWN_TOKEN)
+ emit dropToLoginPageCb(msg);
+ else
+ emit tryDelayedSyncCb();
+
+ return;
+ }
+ }
+ }
+
+ nhlog::net()->debug("sync completed: {}", res.next_batch);
+
+ // Ensure that we have enough one-time keys available.
+ ensureOneTimeKeyCount(res.device_one_time_keys_count);
+
+ // TODO: fine grained error handling
+ try {
+ cache::client()->saveState(res);
+ olm::handle_to_device_messages(res.to_device);
+
+ emit syncUI(res.rooms);
+
+ auto updates = cache::client()->roomUpdates(res);
+
+ emit syncTopBar(updates);
+ emit syncRoomlist(updates);
+ } catch (const lmdb::error &e) {
+ nhlog::db()->error("saving sync response: {}", e.what());
+ }
+
+ emit trySyncCb();
+ });
+}
+
+void
+ChatPage::joinRoom(const QString &room)
+{
+ const auto room_id = room.toStdString();
+
+ http::v2::client()->join_room(
+ room_id, [this, room_id](const nlohmann::json &, mtx::http::RequestErr err) {
+ if (err) {
+ emit showNotification(
+ QString("Failed to join room: %1")
+ .arg(QString::fromStdString(err->matrix_error.error)));
+ return;
+ }
+
+ emit showNotification("You joined the room");
+
+ // We remove any invites with the same room_id.
+ try {
+ cache::client()->removeInvite(room_id);
+ } catch (const lmdb::error &e) {
+ emit showNotification(
+ QString("Failed to remove invite: %1").arg(e.what()));
+ }
+ });
+}
+
+void
+ChatPage::createRoom(const mtx::requests::CreateRoom &req)
+{
+ http::v2::client()->create_room(
+ req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) {
+ if (err) {
+ emit showNotification(
+ tr("Room creation failed: %1")
+ .arg(QString::fromStdString(err->matrix_error.error)));
+ return;
+ }
+
+ emit showNotification(QString("Room %1 created")
+ .arg(QString::fromStdString(res.room_id.to_string())));
+ });
+}
+
+void
+ChatPage::leaveRoom(const QString &room_id)
+{
+ http::v2::client()->leave_room(
+ room_id.toStdString(), [this, room_id](const json &, mtx::http::RequestErr err) {
+ if (err) {
+ emit showNotification(
+ tr("Failed to leave room: %1")
+ .arg(QString::fromStdString(err->matrix_error.error)));
+ return;
+ }
+
+ emit leftRoom(room_id);
+ });
+}
+
+void
+ChatPage::sendTypingNotifications()
+{
+ if (!userSettings_->isTypingNotificationsEnabled())
+ return;
+
+ http::v2::client()->start_typing(
+ current_room_.toStdString(), 10'000, [](mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to send typing notification: {}",
+ err->matrix_error.error);
+ }
+ });
+}
+
+void
+ChatPage::initialSyncHandler(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);
+ 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("sync error: {} {}", status_code, err_code);
+
+ switch (status_code) {
+ case 502:
+ case 504:
+ case 524: {
+ emit tryInitialSyncCb();
+ return;
+ }
+ default: {
+ emit dropToLoginPageCb(msg);
+ return;
+ }
+ }
+ }
+
+ nhlog::net()->info("initial sync completed");
+
+ try {
+ cache::client()->saveState(res);
+
+ olm::handle_to_device_messages(res.to_device);
+
+ emit initializeViews(std::move(res.rooms));
+ emit initializeRoomList(cache::client()->roomInfo());
+ } catch (const lmdb::error &e) {
+ nhlog::db()->error("{}", e.what());
+ emit tryInitialSyncCb();
+ return;
+ }
+
+ emit trySyncCb();
+ emit contentLoaded();
+}
+
+void
+ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts)
+{
+ for (const auto &entry : counts) {
+ if (entry.second < MAX_ONETIME_KEYS) {
+ const int nkeys = MAX_ONETIME_KEYS - entry.second;
+
+ nhlog::crypto()->info("uploading {} {} keys", nkeys, entry.first);
+ olm::client()->generate_one_time_keys(nkeys);
+
+ http::v2::client()->upload_keys(
+ olm::client()->create_upload_keys_request(),
+ [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::crypto()->warn(
+ "failed to update one-time keys: {} {}",
+ err->matrix_error.error,
+ static_cast<int>(err->status_code));
+ return;
+ }
+
+ olm::client()->mark_keys_as_published();
+ });
+ }
+ }
+}
+
+void
+ChatPage::getProfileInfo()
+{
+ QSettings settings;
+ const auto userid = settings.value("auth/user_id").toString().toStdString();
+
+ http::v2::client()->get_profile(
+ userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to retrieve own profile info");
+ return;
+ }
+
+ emit setUserDisplayName(QString::fromStdString(res.display_name));
+
+ if (cache::client()) {
+ auto data = cache::client()->image(res.avatar_url);
+ if (!data.isNull()) {
+ emit setUserAvatar(QImage::fromData(data));
+ return;
+ }
+ }
+
+ if (res.avatar_url.empty())
+ return;
+
+ http::v2::client()->download(
+ res.avatar_url,
+ [this, res](const std::string &data,
+ const std::string &,
+ const std::string &,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn(
+ "failed to download user avatar: {} - {}",
+ mtx::errors::to_string(err->matrix_error.errcode),
+ err->matrix_error.error);
+ return;
+ }
+
+ if (cache::client())
+ cache::client()->saveImage(res.avatar_url, data);
+
+ emit setUserAvatar(
+ QImage::fromData(QByteArray(data.data(), data.size())));
+ });
+ });
+ // TODO http::client()->getOwnCommunities();
+}
diff --git a/src/CommunitiesList.cc b/src/CommunitiesList.cc
index 0d7f5aab..39e9a7fe 100644
--- a/src/CommunitiesList.cc
+++ b/src/CommunitiesList.cc
@@ -1,4 +1,6 @@
#include "CommunitiesList.h"
+#include "Cache.h"
+#include "Logging.hpp"
#include "MatrixClient.h"
#include <QLabel>
@@ -38,17 +40,14 @@ CommunitiesList::CommunitiesList(QWidget *parent)
scrollArea_->setWidget(scrollAreaContents_);
topLayout_->addWidget(scrollArea_);
- connect(http::client(),
- &MatrixClient::communityProfileRetrieved,
- this,
- [](QString communityId, QJsonObject profile) {
- http::client()->fetchCommunityAvatar(
- communityId, QUrl(profile["avatar_url"].toString()));
- });
- connect(http::client(),
- SIGNAL(communityAvatarRetrieved(const QString &, const QPixmap &)),
- this,
- SLOT(updateCommunityAvatar(const QString &, const QPixmap &)));
+ // connect(http::client(),
+ // &MatrixClient::communityProfileRetrieved,
+ // this,
+ // [this](QString communityId, QJsonObject profile) {
+ // fetchCommunityAvatar(communityId, profile["avatar_url"].toString());
+ // });
+ connect(
+ this, &CommunitiesList::avatarRetrieved, this, &CommunitiesList::updateCommunityAvatar);
}
void
@@ -61,8 +60,8 @@ CommunitiesList::setCommunities(const std::map<QString, QSharedPointer<Community
for (const auto &community : communities) {
addCommunity(community.second, community.first);
- http::client()->fetchCommunityProfile(community.first);
- http::client()->fetchCommunityRooms(community.first);
+ // http::client()->fetchCommunityProfile(community.first);
+ // http::client()->fetchCommunityRooms(community.first);
}
communities_["world"]->setPressedState(true);
@@ -77,7 +76,7 @@ CommunitiesList::addCommunity(QSharedPointer<Community> community, const QString
communities_.emplace(community_id, QSharedPointer<CommunitiesListItem>(list_item));
- http::client()->fetchCommunityAvatar(community_id, community->getAvatar());
+ fetchCommunityAvatar(community_id, community->getAvatar().toString());
contentsLayout_->insertWidget(contentsLayout_->count() - 1, list_item);
@@ -117,3 +116,40 @@ CommunitiesList::highlightSelectedCommunity(const QString &community_id)
}
}
}
+
+void
+CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl)
+{
+ auto savedImgData = cache::client()->image(avatarUrl);
+ if (!savedImgData.isNull()) {
+ QPixmap pix;
+ pix.loadFromData(savedImgData);
+ emit avatarRetrieved(id, pix);
+ return;
+ }
+
+ if (avatarUrl.isEmpty())
+ return;
+
+ mtx::http::ThumbOpts opts;
+ opts.mxc_url = avatarUrl.toStdString();
+ http::v2::client()->get_thumbnail(
+ opts, [this, opts, id](const std::string &res, mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to download avatar: {} - ({} {})",
+ opts.mxc_url,
+ mtx::errors::to_string(err->matrix_error.errcode),
+ err->matrix_error.error);
+ return;
+ }
+
+ cache::client()->saveImage(opts.mxc_url, res);
+
+ auto data = QByteArray(res.data(), res.size());
+
+ QPixmap pix;
+ pix.loadFromData(data);
+
+ emit avatarRetrieved(id, pix);
+ });
+}
diff --git a/src/Logging.cpp b/src/Logging.cpp
new file mode 100644
index 00000000..bccbe389
--- /dev/null
+++ b/src/Logging.cpp
@@ -0,0 +1,59 @@
+#include "Logging.hpp"
+
+#include <iostream>
+#include <spdlog/sinks/file_sinks.h>
+
+namespace {
+std::shared_ptr<spdlog::logger> db_logger = nullptr;
+std::shared_ptr<spdlog::logger> net_logger = nullptr;
+std::shared_ptr<spdlog::logger> crypto_logger = nullptr;
+std::shared_ptr<spdlog::logger> ui_logger = nullptr;
+
+constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6;
+constexpr auto MAX_LOG_FILES = 3;
+}
+
+namespace nhlog {
+void
+init(const std::string &file_path)
+{
+ auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
+ file_path, MAX_FILE_SIZE, MAX_LOG_FILES);
+
+ auto console_sink = std::make_shared<spdlog::sinks::stdout_sink_mt>();
+
+ std::vector<spdlog::sink_ptr> sinks;
+ sinks.push_back(file_sink);
+ sinks.push_back(console_sink);
+
+ net_logger = std::make_shared<spdlog::logger>("net", std::begin(sinks), std::end(sinks));
+ ui_logger = std::make_shared<spdlog::logger>("ui", std::begin(sinks), std::end(sinks));
+ db_logger = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks));
+ crypto_logger =
+ std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks));
+}
+
+std::shared_ptr<spdlog::logger>
+ui()
+{
+ return ui_logger;
+}
+
+std::shared_ptr<spdlog::logger>
+net()
+{
+ return net_logger;
+}
+
+std::shared_ptr<spdlog::logger>
+db()
+{
+ return db_logger;
+}
+
+std::shared_ptr<spdlog::logger>
+crypto()
+{
+ return crypto_logger;
+}
+}
diff --git a/src/LoginPage.cc b/src/LoginPage.cc
index c7f9b042..d695a759 100644
--- a/src/LoginPage.cc
+++ b/src/LoginPage.cc
@@ -137,16 +137,16 @@ LoginPage::LoginPage(QWidget *parent)
setLayout(top_layout_);
+ connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk);
+ connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError);
+ connect(this, &LoginPage::loginErrorCb, this, &LoginPage::loginError);
+
connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked()));
connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
- connect(http::client(), SIGNAL(loginError(QString)), this, SLOT(loginError(QString)));
- connect(http::client(), SIGNAL(loginError(QString)), this, SIGNAL(errorOccurred()));
connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered()));
- connect(http::client(), SIGNAL(versionError(QString)), this, SLOT(versionError(QString)));
- connect(http::client(), SIGNAL(versionSuccess()), this, SLOT(versionSuccess()));
connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered()));
}
@@ -180,17 +180,47 @@ LoginPage::onMatrixIdEntered()
inferredServerAddress_ = homeServer;
serverInput_->setText(homeServer);
- http::client()->setServer(homeServer);
- http::client()->versions();
+
+ http::v2::client()->set_server(user.hostname());
+ checkHomeserverVersion();
}
}
void
+LoginPage::checkHomeserverVersion()
+{
+ http::v2::client()->versions(
+ [this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
+ if (err) {
+ using namespace boost::beast::http;
+
+ if (err->status_code == status::not_found) {
+ emit versionErrorCb(tr("The required endpoints were not found. "
+ "Possibly not a Matrix server."));
+ return;
+ }
+
+ if (!err->parse_error.empty()) {
+ emit versionErrorCb(tr("Received malformed response. Make sure "
+ "the homeserver domain is valid."));
+ return;
+ }
+
+ emit versionErrorCb(tr(
+ "An unknown error occured. Make sure the homeserver domain is valid."));
+ return;
+ }
+
+ emit versionOkCb();
+ });
+}
+
+void
LoginPage::onServerAddressEntered()
{
error_label_->setText("");
- http::client()->setServer(serverInput_->text());
- http::client()->versions();
+ http::v2::client()->set_server(serverInput_->text().toStdString());
+ checkHomeserverVersion();
serverLayout_->removeWidget(errorIcon_);
errorIcon_->hide();
@@ -199,11 +229,8 @@ LoginPage::onServerAddressEntered()
}
void
-LoginPage::versionError(QString error)
+LoginPage::versionError(const QString &error)
{
- QUrl currentServer = http::client()->getHomeServer();
- QString mxidAddress = matrixid_input_->text().split(":").at(1);
-
error_label_->setText(error);
serverInput_->show();
@@ -215,7 +242,7 @@ LoginPage::versionError(QString error)
}
void
-LoginPage::versionSuccess()
+LoginPage::versionOk()
{
serverLayout_->removeWidget(spinner_);
matrixidLayout_->removeWidget(spinner_);
@@ -241,8 +268,20 @@ LoginPage::onLoginButtonClicked()
if (password_input_->text().isEmpty())
return loginError(tr("Empty password"));
- http::client()->setServer(serverInput_->text());
- http::client()->login(QString::fromStdString(user.localpart()), password_input_->text());
+ http::v2::client()->set_server(serverInput_->text().toStdString());
+ http::v2::client()->login(
+ user.localpart(),
+ password_input_->text().toStdString(),
+ initialDeviceName(),
+ [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
+ if (err) {
+ emit loginError(QString::fromStdString(err->matrix_error.error));
+ emit errorOccurred();
+ return;
+ }
+
+ emit loginOk(res);
+ });
emit loggingIn();
}
diff --git a/src/MainWindow.cc b/src/MainWindow.cc
index c46cbff1..088bb5c0 100644
--- a/src/MainWindow.cc
+++ b/src/MainWindow.cc
@@ -17,7 +17,6 @@
#include <QApplication>
#include <QLayout>
-#include <QNetworkReply>
#include <QSettings>
#include <QShortcut>
@@ -26,6 +25,7 @@
#include "ChatPage.h"
#include "Config.h"
#include "LoadingIndicator.h"
+#include "Logging.hpp"
#include "LoginPage.h"
#include "MainWindow.h"
#include "MatrixClient.h"
@@ -54,9 +54,6 @@ MainWindow::MainWindow(QWidget *parent)
setWindowTitle("nheko");
setObjectName("MainWindow");
- // Initialize the http client.
- http::init();
-
restoreWindowSize();
QFont font("Open Sans");
@@ -124,21 +121,13 @@ MainWindow::MainWindow(QWidget *parent)
connect(
chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage);
- connect(http::client(),
- SIGNAL(loginSuccess(QString, QString, QString)),
- this,
- SLOT(showChatPage(QString, QString, QString)));
-
- connect(http::client(),
- SIGNAL(registerSuccess(QString, QString, QString)),
- this,
- SLOT(showChatPage(QString, QString, QString)));
- connect(http::client(), &MatrixClient::invalidToken, this, [this]() {
- chat_page_->deleteConfigs();
- showLoginPage();
- login_page_->loginError("Invalid token detected. Please try to login again.");
+ connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) {
+ http::v2::client()->set_user(res.user_id);
+ showChatPage();
});
+ connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage);
+
QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this);
connect(quitShortcut, &QShortcut::activated, this, QApplication::quit);
@@ -156,8 +145,21 @@ MainWindow::MainWindow(QWidget *parent)
QString token = settings.value("auth/access_token").toString();
QString home_server = settings.value("auth/home_server").toString();
QString user_id = settings.value("auth/user_id").toString();
+ QString device_id = settings.value("auth/device_id").toString();
+
+ http::v2::client()->set_access_token(token.toStdString());
+ http::v2::client()->set_server(home_server.toStdString());
+ http::v2::client()->set_device_id(device_id.toStdString());
- showChatPage(user_id, home_server, token);
+ try {
+ using namespace mtx::identifiers;
+ http::v2::client()->set_user(parse<User>(user_id.toStdString()));
+ } catch (const std::invalid_argument &e) {
+ nhlog::ui()->critical("bootstrapped with invalid user_id: {}",
+ user_id.toStdString());
+ }
+
+ showChatPage();
}
}
@@ -216,12 +218,19 @@ MainWindow::removeOverlayProgressBar()
}
void
-MainWindow::showChatPage(QString userid, QString homeserver, QString token)
+MainWindow::showChatPage()
{
+ auto userid = QString::fromStdString(http::v2::client()->user_id().to_string());
+ auto device_id = QString::fromStdString(http::v2::client()->device_id());
+ auto homeserver = QString::fromStdString(http::v2::client()->server() + ":" +
+ std::to_string(http::v2::client()->port()));
+ auto token = QString::fromStdString(http::v2::client()->access_token());
+
QSettings settings;
settings.setValue("auth/access_token", token);
settings.setValue("auth/home_server", homeserver);
settings.setValue("auth/user_id", userid);
+ settings.setValue("auth/device_id", device_id);
showOverlayProgressBar();
@@ -317,7 +326,7 @@ MainWindow::openLeaveRoomDialog(const QString &room_id)
leaveRoomModal_->hide();
if (leaving)
- http::client()->leaveRoom(roomToLeave);
+ chat_page_->leaveRoom(roomToLeave);
});
leaveRoomModal_ =
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index a9720d10..d4ab8e33 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -1,1368 +1,38 @@
-/*
- * 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 <QDebug>
-#include <QFile>
-#include <QImageReader>
-#include <QJsonArray>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QMimeDatabase>
-#include <QNetworkReply>
-#include <QNetworkRequest>
-#include <QPixmap>
-#include <QProcessEnvironment>
-#include <QSettings>
-#include <QUrlQuery>
-#include <QtConcurrent>
-#include <mtx/errors.hpp>
-
#include "MatrixClient.h"
+#include <memory>
+
namespace {
-std::unique_ptr<MatrixClient> instance_ = nullptr;
+auto v2_client_ = std::make_shared<mtx::http::Client>();
}
namespace http {
+namespace v2 {
-void
-init()
-{
- if (!instance_)
- instance_ = std::make_unique<MatrixClient>();
-}
-
-MatrixClient *
+mtx::http::Client *
client()
{
- return instance_.get();
-}
-}
-
-MatrixClient::MatrixClient(QObject *parent)
- : QNetworkAccessManager(parent)
- , clientApiUrl_{"/_matrix/client/r0"}
- , mediaApiUrl_{"/_matrix/media/r0"}
- , serverProtocol_{"https"}
-{
- qRegisterMetaType<mtx::responses::Sync>();
-
- QSettings settings;
- txn_id_ = settings.value("client/transaction_id", 1).toInt();
-
- auto env = QProcessEnvironment::systemEnvironment();
-
- auto allowInsecureConnections = env.value("NHEKO_ALLOW_INSECURE_CONNECTIONS", "0");
-
- if (allowInsecureConnections == "1") {
- qWarning() << "Insecure connections are allowed: SSL errors will be ignored";
- connect(
- this,
- &QNetworkAccessManager::sslErrors,
- this,
- [](QNetworkReply *reply, const QList<QSslError> &) { reply->ignoreSslErrors(); });
- }
-
- QJsonObject default_filter{
- {
- "room",
- QJsonObject{
- {"include_leave", true},
- {
- "account_data",
- QJsonObject{
- {"not_types", QJsonArray{"*"}},
- },
- },
- },
- },
- {
- "account_data",
- QJsonObject{
- {"not_types", QJsonArray{"*"}},
- },
- },
- {
- "presence",
- QJsonObject{
- {"not_types", QJsonArray{"*"}},
- },
- },
- };
-
- filter_ = settings
- .value("client/sync_filter",
- QJsonDocument(default_filter).toJson(QJsonDocument::Compact))
- .toString();
-
- connect(this,
- &QNetworkAccessManager::networkAccessibleChanged,
- this,
- [this](NetworkAccessibility status) {
- if (status != NetworkAccessibility::Accessible)
- setNetworkAccessible(NetworkAccessibility::Accessible);
- });
-}
-
-void
-MatrixClient::reset() noexcept
-{
- next_batch_.clear();
- server_.clear();
- token_.clear();
-
- txn_id_ = 0;
-}
-
-void
-MatrixClient::login(const QString &username, const QString &password) noexcept
-{
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + "/login");
-
- QNetworkRequest request(endpoint);
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
-
- mtx::requests::Login login;
- login.user = username.toStdString();
- login.password = password.toStdString();
- login.initial_device_display_name = "nheko";
-
-#if defined(Q_OS_MAC)
- login.initial_device_display_name = "nheko on Mac OS";
-#elif defined(Q_OS_LINUX)
- login.initial_device_display_name = "nheko on Linux";
-#elif defined(Q_OS_WIN)
- login.initial_device_display_name = "nheko on Windows";
-#endif
-
- json j = login;
-
- auto data = QByteArray::fromStdString(j.dump());
- auto reply = post(request, data);
- connect(reply, &QNetworkReply::finished, this, [this, reply]() {
- reply->deleteLater();
-
- int status_code =
- reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status_code == 403) {
- emit loginError(tr("Wrong username or password"));
- return;
- }
-
- if (status_code == 404) {
- emit loginError(tr("Login endpoint was not found on the server"));
- return;
- }
-
- if (status_code >= 400) {
- qWarning() << "Login error: " << reply->errorString();
- emit loginError(tr("An unknown error occured. Please try again."));
- return;
- }
-
- if (reply->error()) {
- emit loginError(reply->errorString());
- return;
- }
-
- try {
- mtx::responses::Login login =
- nlohmann::json::parse(reply->readAll().data());
-
- auto hostname = server_.host();
-
- if (server_.port() > 0)
- hostname = QString("%1:%2").arg(server_.host()).arg(server_.port());
-
- emit loginSuccess(QString::fromStdString(login.user_id.to_string()),
- hostname,
- QString::fromStdString(login.access_token));
- } catch (std::exception &e) {
- qWarning() << "Malformed JSON response" << e.what();
- emit loginError(tr("Malformed response. Possibly not a Matrix server"));
- }
- });
-}
-void
-MatrixClient::logout() noexcept
-{
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + "/logout");
-
- QNetworkRequest request(endpoint);
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- setupAuth(request);
-
- QJsonObject body{};
- auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-
- connect(reply, &QNetworkReply::finished, this, [this, reply]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status != 200) {
- qWarning() << "Logout error: " << reply->errorString();
- return;
- }
-
- emit loggedOut();
- });
-}
-
-void
-MatrixClient::registerUser(const QString &user,
- const QString &pass,
- const QString &server,
- const QString &session) noexcept
-{
- setServer(server);
-
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + "/register");
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
-
- QJsonObject body{{"username", user}, {"password", pass}};
-
- // We trying to register using the response from the recaptcha.
- if (!session.isEmpty())
- body = QJsonObject{
- {"username", user},
- {"password", pass},
- {"auth", QJsonObject{{"type", "m.login.recaptcha"}, {"session", session}}}};
-
- auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-
- connect(reply, &QNetworkReply::finished, this, [this, reply, user, pass, server]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- auto data = reply->readAll();
-
- // Try to parse a regular register response.
- try {
- mtx::responses::Register res = nlohmann::json::parse(data);
- emit registerSuccess(QString::fromStdString(res.user_id.to_string()),
- QString::fromStdString(res.user_id.hostname()),
- QString::fromStdString(res.access_token));
- } catch (const std::exception &e) {
- qWarning() << "Register" << e.what();
- }
-
- // Check if the server requires a registration flow.
- try {
- mtx::responses::RegistrationFlows res = nlohmann::json::parse(data);
- emit registrationFlow(
- user, pass, server, QString::fromStdString(res.session));
- return;
- } catch (const std::exception &) {
- }
-
- // We encountered an unknown error.
- if (status == 0 || status >= 400) {
- try {
- mtx::errors::Error res = nlohmann::json::parse(data);
- emit registerError(QString::fromStdString(res.error));
- return;
- } catch (const std::exception &) {
- }
-
- emit registerError(reply->errorString());
- }
- });
-}
-
-void
-MatrixClient::sync() noexcept
-{
- // the filter is not uploaded yet (so it is a json with { at the beginning)
- // ignore for now that the filter might be uploaded multiple times as we expect
- // servers to do deduplication
- if (filter_.startsWith("{")) {
- uploadFilter(filter_);
- }
-
- QUrlQuery query;
- query.addQueryItem("set_presence", "online");
- query.addQueryItem("filter", filter_);
- query.addQueryItem("timeout", "30000");
-
- if (next_batch_.isEmpty()) {
- qDebug() << "Sync requires a valid next_batch token. Initial sync should "
- "be performed.";
- return;
- }
-
- query.addQueryItem("since", next_batch_);
-
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + "/sync");
- endpoint.setQuery(query);
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- setupAuth(request);
-
- auto reply = get(request);
- connect(reply, &QNetworkReply::finished, this, [this, reply]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
- auto data = reply->readAll();
-
- if (status == 0 || status >= 400) {
- try {
- mtx::errors::Error res = nlohmann::json::parse(data);
-
- if (res.errcode == mtx::errors::ErrorCode::M_UNKNOWN_TOKEN) {
- emit invalidToken();
- return;
- }
-
- emit syncError(QString::fromStdString(res.error));
-
- return;
- } catch (const nlohmann::json::exception &e) {
- qWarning() << e.what();
- }
- }
-
- try {
- emit syncCompleted(nlohmann::json::parse(std::move(data)));
- } catch (std::exception &e) {
- qWarning() << "Sync error: " << e.what();
- }
- });
-}
-
-void
-MatrixClient::sendRoomMessage(mtx::events::MessageType ty,
- int txnId,
- const QString &roomid,
- const QString &msg,
- const QString &mime,
- uint64_t media_size,
- const QString &url) noexcept
-{
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ +
- QString("/rooms/%1/send/m.room.message/%2").arg(roomid).arg(txnId));
-
- QJsonObject body;
- QJsonObject info = {{"size", static_cast<qint64>(media_size)}, {"mimetype", mime}};
-
- switch (ty) {
- case mtx::events::MessageType::Text:
- body = {{"msgtype", "m.text"}, {"body", msg}};
- break;
- case mtx::events::MessageType::Emote:
- body = {{"msgtype", "m.emote"}, {"body", msg}};
- break;
- case mtx::events::MessageType::Image:
- body = {{"msgtype", "m.image"}, {"body", msg}, {"url", url}, {"info", info}};
- break;
- case mtx::events::MessageType::File:
- body = {{"msgtype", "m.file"}, {"body", msg}, {"url", url}, {"info", info}};
- break;
- case mtx::events::MessageType::Audio:
- body = {{"msgtype", "m.audio"}, {"body", msg}, {"url", url}, {"info", info}};
- break;
- case mtx::events::MessageType::Video:
- body = {{"msgtype", "m.video"}, {"body", msg}, {"url", url}, {"info", info}};
- break;
- default:
- qDebug() << "SendRoomMessage: Unknown message type for" << msg;
- return;
- }
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- setupAuth(request);
-
- auto reply = put(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-
- connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, txnId]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- emit messageSendFailed(roomid, txnId);
- return;
- }
-
- auto data = reply->readAll();
-
- if (data.isEmpty()) {
- emit messageSendFailed(roomid, txnId);
- return;
- }
-
- auto json = QJsonDocument::fromJson(data);
-
- if (!json.isObject()) {
- qDebug() << "Send message response is not a JSON object";
- emit messageSendFailed(roomid, txnId);
- return;
- }
-
- auto object = json.object();
-
- if (!object.contains("event_id")) {
- qDebug() << "SendTextMessage: missing event_id from response";
- emit messageSendFailed(roomid, txnId);
- return;
- }
-
- emit messageSent(object.value("event_id").toString(), roomid, txnId);
- });
-}
-
-void
-MatrixClient::initialSync() noexcept
-{
- QUrlQuery query;
- query.addQueryItem("timeout", "0");
- query.addQueryItem("filter", filter_);
-
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + "/sync");
- endpoint.setQuery(query);
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- setupAuth(request);
-
- auto reply = get(request);
- connect(reply, &QNetworkReply::finished, this, [this, reply]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- qDebug() << "Error code received" << status;
- emit initialSyncFailed(status);
- return;
- }
-
- QtConcurrent::run([data = reply->readAll(), this]() {
- try {
- emit initialSyncCompleted(nlohmann::json::parse(std::move(data)));
- } catch (std::exception &e) {
- qWarning() << "Initial sync error:" << e.what();
- emit initialSyncFailed();
- }
- });
- });
-}
-
-void
-MatrixClient::versions() noexcept
-{
- QUrl endpoint(server_);
- endpoint.setPath("/_matrix/client/versions");
-
- QNetworkRequest request(endpoint);
-
- auto reply = get(request);
- connect(reply, &QNetworkReply::finished, this, [this, reply]() {
- reply->deleteLater();
-
- int status_code =
- reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (reply->error()) {
- emit versionError(reply->errorString());
- return;
- }
-
- if (status_code == 404) {
- emit versionError("Versions endpoint was not found on the server. Possibly "
- "not a Matrix server");
- return;
- }
-
- if (status_code >= 400) {
- emit versionError("An unknown error occured. Please try again.");
- return;
- }
-
- try {
- mtx::responses::Versions versions =
- nlohmann::json::parse(reply->readAll().data());
-
- emit versionSuccess();
- } catch (std::exception &e) {
- emit versionError("Malformed response. Possibly not a Matrix server");
- }
- });
-}
-
-void
-MatrixClient::getOwnProfile() noexcept
-{
- // FIXME: Remove settings from the matrix client. The class should store the
- // user's matrix ID.
- QSettings settings;
- auto userid = settings.value("auth/user_id", "").toString();
-
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + "/profile/" + userid);
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- setupAuth(request);
-
- QNetworkReply *reply = get(request);
- connect(reply, &QNetworkReply::finished, this, [this, reply]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status >= 400) {
- qWarning() << reply->errorString();
- return;
- }
-
- try {
- mtx::responses::Profile profile =
- nlohmann::json::parse(reply->readAll().data());
-
- emit getOwnProfileResponse(QUrl(QString::fromStdString(profile.avatar_url)),
- QString::fromStdString(profile.display_name));
- } catch (std::exception &e) {
- qWarning() << "Profile:" << e.what();
- }
- });
-}
-
-void
-MatrixClient::getOwnCommunities() noexcept
-{
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + "/joined_groups");
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- setupAuth(request);
-
- QNetworkReply *reply = get(request);
- connect(reply, &QNetworkReply::finished, this, [this, reply]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status >= 400) {
- qWarning() << reply->errorString();
- return;
- }
-
- auto data = reply->readAll();
- auto json = QJsonDocument::fromJson(data).object();
-
- if (!json.contains("groups")) {
- qWarning() << "failed to parse own communities. 'groups' key not found";
- return;
- }
-
- QList<QString> response;
- for (auto group : json["groups"].toArray())
- response.append(group.toString());
-
- emit getOwnCommunitiesResponse(response);
- });
-}
-
-void
-MatrixClient::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url)
-{
- QList<QString> url_parts = avatar_url.toString().split("mxc://");
-
- if (url_parts.size() != 2) {
- qDebug() << "Invalid format for room avatar " << avatar_url.toString();
- return;
- }
-
- QUrlQuery query;
- query.addQueryItem("width", "512");
- query.addQueryItem("height", "512");
- query.addQueryItem("method", "crop");
-
- QString media_url =
- QString("%1/_matrix/media/r0/thumbnail/%2").arg(getHomeServer().toString(), url_parts[1]);
-
- QUrl endpoint(media_url);
- endpoint.setQuery(query);
-
- QNetworkRequest avatar_request(endpoint);
-
- QNetworkReply *reply = get(avatar_request);
- connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, avatar_url]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- qWarning() << reply->errorString();
- return;
- }
-
- auto img = reply->readAll();
-
- if (img.size() == 0)
- return;
-
- QPixmap pixmap;
- pixmap.loadFromData(img);
-
- emit roomAvatarRetrieved(roomid, pixmap, avatar_url.toString(), img);
- });
-}
-
-void
-MatrixClient::fetchCommunityAvatar(const QString &communityId, const QUrl &avatar_url)
-{
- if (avatar_url.isEmpty())
- return;
-
- QList<QString> url_parts = avatar_url.toString().split("mxc://");
-
- if (url_parts.size() != 2) {
- qDebug() << "Invalid format for community avatar " << avatar_url.toString();
- return;
- }
-
- QUrlQuery query;
- query.addQueryItem("width", "512");
- query.addQueryItem("height", "512");
- query.addQueryItem("method", "crop");
-
- QString media_url =
- QString("%1/_matrix/media/r0/thumbnail/%2").arg(getHomeServer().toString(), url_parts[1]);
-
- QUrl endpoint(media_url);
- endpoint.setQuery(query);
-
- QNetworkRequest avatar_request(endpoint);
-
- QNetworkReply *reply = get(avatar_request);
- connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- qWarning() << reply->errorString();
- return;
- }
-
- auto img = reply->readAll();
-
- if (img.size() == 0)
- return;
-
- QPixmap pixmap;
- pixmap.loadFromData(img);
-
- emit communityAvatarRetrieved(communityId, pixmap);
- });
-}
-
-void
-MatrixClient::fetchCommunityProfile(const QString &communityId)
-{
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + "/groups/" + communityId + "/profile");
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- setupAuth(request);
-
- QNetworkReply *reply = get(request);
-
- connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status >= 400) {
- qWarning() << reply->errorString();
- return;
- }
-
- auto data = reply->readAll();
- const auto json = QJsonDocument::fromJson(data).object();
-
- emit communityProfileRetrieved(communityId, json);
- });
-}
-
-void
-MatrixClient::fetchCommunityRooms(const QString &communityId)
-{
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + "/groups/" + communityId + "/rooms");
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- setupAuth(request);
-
- QNetworkReply *reply = get(request);
- connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status >= 400) {
- qWarning() << reply->errorString();
- return;
- }
-
- auto data = reply->readAll();
- const auto json = QJsonDocument::fromJson(data).object();
-
- emit communityRoomsRetrieved(communityId, json);
- });
-}
-
-QSharedPointer<DownloadMediaProxy>
-MatrixClient::fetchUserAvatar(const QUrl &avatarUrl)
-{
- QList<QString> url_parts = avatarUrl.toString().split("mxc://");
-
- if (url_parts.size() != 2)
- return QSharedPointer<DownloadMediaProxy>();
-
- QUrlQuery query;
- query.addQueryItem("width", "128");
- query.addQueryItem("height", "128");
- query.addQueryItem("method", "crop");
-
- QString media_url =
- QString("%1/_matrix/media/r0/thumbnail/%2").arg(getHomeServer().toString(), url_parts[1]);
-
- QUrl endpoint(media_url);
- endpoint.setQuery(query);
-
- QNetworkRequest avatar_request(endpoint);
-
- auto reply = get(avatar_request);
- auto proxy = QSharedPointer<DownloadMediaProxy>(new DownloadMediaProxy,
- [](auto proxy) { proxy->deleteLater(); });
- connect(reply, &QNetworkReply::finished, this, [reply, proxy, avatarUrl]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- qWarning() << reply->errorString() << avatarUrl;
- return;
- }
-
- auto data = reply->readAll();
-
- if (data.size() == 0) {
- qWarning() << "received avatar with no data:" << avatarUrl;
- return;
- }
-
- QImage img;
- img.loadFromData(data);
-
- emit proxy->avatarDownloaded(img);
- });
-
- return proxy;
-}
-
-QSharedPointer<DownloadMediaProxy>
-MatrixClient::downloadImage(const QUrl &url)
-{
- QNetworkRequest image_request(url);
-
- auto reply = get(image_request);
- auto proxy = QSharedPointer<DownloadMediaProxy>(new DownloadMediaProxy,
- [](auto proxy) { proxy->deleteLater(); });
- connect(reply, &QNetworkReply::finished, this, [reply, proxy]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- qWarning() << reply->errorString();
- return;
- }
-
- auto img = reply->readAll();
-
- if (img.size() == 0)
- return;
-
- QPixmap pixmap;
- pixmap.loadFromData(img);
-
- emit proxy->imageDownloaded(pixmap);
- });
-
- return proxy;
-}
-
-QSharedPointer<DownloadMediaProxy>
-MatrixClient::downloadFile(const QUrl &url)
-{
- QNetworkRequest fileRequest(url);
-
- auto reply = get(fileRequest);
- auto proxy = QSharedPointer<DownloadMediaProxy>(new DownloadMediaProxy,
- [](auto proxy) { proxy->deleteLater(); });
- connect(reply, &QNetworkReply::finished, this, [reply, proxy]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- // TODO: Handle error
- qWarning() << reply->errorString();
- return;
- }
-
- auto data = reply->readAll();
-
- if (data.size() == 0)
- return;
-
- emit proxy->fileDownloaded(data);
- });
-
- return proxy;
-}
-
-void
-MatrixClient::messages(const QString &roomid, const QString &from_token, int limit) noexcept
-{
- QUrlQuery query;
- query.addQueryItem("from", from_token);
- query.addQueryItem("dir", "b");
- query.addQueryItem("limit", QString::number(limit));
-
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/messages").arg(roomid));
- endpoint.setQuery(query);
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- setupAuth(request);
-
- auto reply = get(request);
- connect(reply, &QNetworkReply::finished, this, [this, reply, roomid]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- qWarning() << reply->errorString();
- return;
- }
-
- try {
- mtx::responses::Messages messages =
- nlohmann::json::parse(reply->readAll().data());
-
- emit messagesRetrieved(roomid, messages);
- } catch (std::exception &e) {
- qWarning() << "Room messages from" << roomid << e.what();
- return;
- }
- });
-}
-
-void
-MatrixClient::uploadImage(const QString &roomid,
- const QString &filename,
- const QSharedPointer<QIODevice> data)
-{
- auto reply = makeUploadRequest(data);
-
- if (reply == nullptr)
- return;
-
- connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename, data]() {
- auto json = getUploadReply(reply);
- if (json.isEmpty())
- return;
-
- auto mime = reply->request().header(QNetworkRequest::ContentTypeHeader).toString();
- auto size =
- reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong();
-
- emit imageUploaded(
- roomid, filename, json.value("content_uri").toString(), mime, size);
- });
-}
-
-void
-MatrixClient::uploadFile(const QString &roomid,
- const QString &filename,
- const QSharedPointer<QIODevice> data)
-{
- auto reply = makeUploadRequest(data);
-
- if (reply == nullptr)
- return;
-
- connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename, data]() {
- auto json = getUploadReply(reply);
- if (json.isEmpty())
- return;
-
- auto mime = reply->request().header(QNetworkRequest::ContentTypeHeader).toString();
- auto size =
- reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong();
-
- emit fileUploaded(
- roomid, filename, json.value("content_uri").toString(), mime, size);
- });
-}
-
-void
-MatrixClient::uploadAudio(const QString &roomid,
- const QString &filename,
- const QSharedPointer<QIODevice> data)
-{
- auto reply = makeUploadRequest(data);
-
- if (reply == nullptr)
- return;
-
- connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename, data]() {
- auto json = getUploadReply(reply);
- if (json.isEmpty())
- return;
-
- auto mime = reply->request().header(QNetworkRequest::ContentTypeHeader).toString();
- auto size =
- reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong();
-
- emit audioUploaded(
- roomid, filename, json.value("content_uri").toString(), mime, size);
- });
-}
-
-void
-MatrixClient::uploadVideo(const QString &roomid,
- const QString &filename,
- const QSharedPointer<QIODevice> data)
-{
- auto reply = makeUploadRequest(data);
-
- if (reply == nullptr)
- return;
-
- connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename, data]() {
- auto json = getUploadReply(reply);
- if (json.isEmpty())
- return;
-
- auto mime = reply->request().header(QNetworkRequest::ContentTypeHeader).toString();
- auto size =
- reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong();
-
- emit videoUploaded(
- roomid, filename, json.value("content_uri").toString(), mime, size);
- });
-}
-
-void
-MatrixClient::uploadFilter(const QString &filter) noexcept
-{
- // validate that filter is a Json-String
- QJsonDocument doc = QJsonDocument::fromJson(filter.toUtf8());
- if (doc.isNull() || !doc.isObject()) {
- qWarning() << "Input which should be uploaded as filter is no JsonObject";
- return;
- }
-
- QSettings settings;
- auto userid = settings.value("auth/user_id", "").toString();
-
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + QString("/user/%1/filter").arg(userid));
-
- QNetworkRequest request(endpoint);
- request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
- setupAuth(request);
-
- auto reply = post(request, doc.toJson(QJsonDocument::Compact));
-
- connect(reply, &QNetworkReply::finished, this, [this, reply]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- qWarning() << reply->errorString() << "42";
- return;
- }
-
- auto data = reply->readAll();
- auto response = QJsonDocument::fromJson(data);
- auto filter_id = response.object()["filter_id"].toString();
-
- qDebug() << "Filter with ID" << filter_id << "created.";
- QSettings settings;
- settings.setValue("client/sync_filter", filter_id);
- settings.sync();
-
- // set the filter_ var so following syncs will use it
- filter_ = filter_id;
- });
-}
-
-void
-MatrixClient::joinRoom(const QString &roomIdOrAlias)
-{
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + QString("/join/%1").arg(roomIdOrAlias));
-
- QNetworkRequest request(endpoint);
- request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
- setupAuth(request);
-
- auto reply = post(request, "{}");
- connect(reply, &QNetworkReply::finished, this, [this, reply]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- auto data = reply->readAll();
- auto response = QJsonDocument::fromJson(data);
- auto json = response.object();
-
- if (json.contains("error"))
- emit joinFailed(json["error"].toString());
- else
- qDebug() << reply->errorString();
-
- return;
- }
-
- auto data = reply->readAll();
- auto response = QJsonDocument::fromJson(data);
- auto room_id = response.object()["room_id"].toString();
-
- emit joinedRoom(room_id);
- });
-}
-
-void
-MatrixClient::leaveRoom(const QString &roomId)
-{
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/leave").arg(roomId));
-
- QNetworkRequest request(endpoint);
- request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
- setupAuth(request);
-
- auto reply = post(request, "{}");
-
- connect(reply, &QNetworkReply::finished, this, [this, reply, roomId]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- qWarning() << reply->errorString();
- return;
- }
-
- emit leftRoom(roomId);
- });
-}
-
-void
-MatrixClient::inviteUser(const QString &roomId, const QString &user)
-{
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/invite").arg(roomId));
-
- QNetworkRequest request(endpoint);
- request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
- setupAuth(request);
-
- QJsonObject body{{"user_id", user}};
- auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-
- connect(reply, &QNetworkReply::finished, this, [this, reply, roomId, user]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- // TODO: Handle failure.
- qWarning() << reply->errorString();
- return;
- }
-
- emit invitedUser(roomId, user);
- });
-}
-
-void
-MatrixClient::createRoom(const mtx::requests::CreateRoom &create_room_request)
-{
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + QString("/createRoom"));
-
- QNetworkRequest request(endpoint);
- request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
- setupAuth(request);
-
- nlohmann::json body = create_room_request;
- auto reply = post(request, QString::fromStdString(body.dump()).toUtf8());
-
- connect(reply, &QNetworkReply::finished, this, [this, reply]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- auto data = reply->readAll();
- auto response = QJsonDocument::fromJson(data);
- auto json = response.object();
-
- if (json.contains("error"))
- emit roomCreationFailed(json["error"].toString());
- else
- qDebug() << reply->errorString();
-
- return;
- }
-
- auto data = reply->readAll();
- auto response = QJsonDocument::fromJson(data);
- auto room_id = response.object()["room_id"].toString();
-
- emit roomCreated(room_id);
- });
-}
-
-void
-MatrixClient::sendTypingNotification(const QString &roomid, int timeoutInMillis)
-{
- QSettings settings;
- QString user_id = settings.value("auth/user_id").toString();
-
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/typing/%2").arg(roomid).arg(user_id));
-
- QString msgType("");
- QJsonObject body;
-
- body = {{"typing", true}, {"timeout", timeoutInMillis}};
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- setupAuth(request);
-
- put(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-}
-
-void
-MatrixClient::removeTypingNotification(const QString &roomid)
-{
- QSettings settings;
- QString user_id = settings.value("auth/user_id").toString();
-
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/typing/%2").arg(roomid).arg(user_id));
-
- QString msgType("");
- QJsonObject body;
-
- body = {{"typing", false}};
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- setupAuth(request);
-
- put(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
+ return v2_client_.get();
}
-void
-MatrixClient::readEvent(const QString &room_id, const QString &event_id)
+bool
+is_logged_in()
{
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/read_markers").arg(room_id));
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
- setupAuth(request);
-
- QJsonObject body({{"m.fully_read", event_id}, {"m.read", event_id}});
- auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-
- connect(reply, &QNetworkReply::finished, this, [reply]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- qWarning() << reply->errorString();
- return;
- }
- });
-}
-
-QNetworkReply *
-MatrixClient::makeUploadRequest(QSharedPointer<QIODevice> iodev)
-{
- QUrl endpoint(server_);
- endpoint.setPath(mediaApiUrl_ + "/upload");
-
- if (!iodev->open(QIODevice::ReadOnly)) {
- qWarning() << "Error while reading device:" << iodev->errorString();
- return nullptr;
- }
-
- QMimeDatabase db;
- QMimeType mime = db.mimeTypeForData(iodev.data());
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- request.setHeader(QNetworkRequest::ContentTypeHeader, mime.name());
- setupAuth(request);
-
- auto reply = post(request, iodev.data());
-
- return reply;
+ return !v2_client_->access_token().empty();
}
-QJsonObject
-MatrixClient::getUploadReply(QNetworkReply *reply)
-{
- QJsonObject object;
-
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (status == 0 || status >= 400) {
- emit uploadFailed(status,
- QString("Media upload failed - %1").arg(reply->errorString()));
- return object;
- }
-
- auto res_data = reply->readAll();
-
- if (res_data.isEmpty()) {
- emit uploadFailed(status, "Media upload failed - Empty response");
- return object;
- }
-
- auto json = QJsonDocument::fromJson(res_data);
-
- if (!json.isObject()) {
- emit uploadFailed(status, "Media upload failed - Invalid response");
- return object;
- }
-
- object = json.object();
- if (!object.contains("content_uri")) {
- emit uploadFailed(status, "Media upload failed - Missing 'content_uri'");
- return QJsonObject{};
- }
-
- return object;
-}
+} // namespace v2
void
-MatrixClient::redactEvent(const QString &room_id, const QString &event_id)
+init()
{
- QUrl endpoint(server_);
- endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/redact/%2/%3")
- .arg(room_id)
- .arg(event_id)
- .arg(incrementTransactionId()));
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
- setupAuth(request);
-
- // TODO: no reason specified
- QJsonObject body{};
- auto reply = put(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-
- connect(reply, &QNetworkReply::finished, this, [reply, this, room_id, event_id]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
- auto data = reply->readAll();
-
- if (status == 0 || status >= 400) {
- try {
- mtx::errors::Error res = nlohmann::json::parse(data);
- emit redactionFailed(QString::fromStdString(res.error));
- return;
- } catch (const std::exception &) {
- }
- }
-
- try {
- mtx::responses::EventId res = nlohmann::json::parse(data);
- emit redactionCompleted(room_id, event_id);
- } catch (const std::exception &e) {
- emit redactionFailed(QString::fromStdString(e.what()));
- }
- });
+ qRegisterMetaType<mtx::responses::Login>();
+ qRegisterMetaType<mtx::responses::Messages>();
+ qRegisterMetaType<mtx::responses::Notifications>();
+ qRegisterMetaType<mtx::responses::Rooms>();
+ qRegisterMetaType<mtx::responses::Sync>();
+ qRegisterMetaType<std::string>();
+ qRegisterMetaType<std::vector<std::string>>();
}
-void
-MatrixClient::getNotifications() noexcept
-{
- QUrlQuery query;
- query.addQueryItem("limit", "5");
-
- QUrl endpoint(server_);
- endpoint.setQuery(query);
- endpoint.setPath(clientApiUrl_ + "/notifications");
-
- QNetworkRequest request(QString(endpoint.toEncoded()));
- setupAuth(request);
-
- auto reply = get(request);
- connect(reply, &QNetworkReply::finished, this, [reply, this]() {
- reply->deleteLater();
-
- int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
- auto data = reply->readAll();
-
- if (status == 0 || status >= 400) {
- try {
- mtx::errors::Error res = nlohmann::json::parse(data);
- std::cout << nlohmann::json::parse(data).dump(2) << '\n';
- // TODO: Response with an error signal
- return;
- } catch (const std::exception &) {
- }
- }
-
- try {
- emit notificationsRetrieved(nlohmann::json::parse(data));
- } catch (const std::exception &e) {
- qWarning() << "failed to parse /notifications response" << e.what();
- }
- });
-}
+} // namespace http
diff --git a/src/Olm.cpp b/src/Olm.cpp
new file mode 100644
index 00000000..814fce18
--- /dev/null
+++ b/src/Olm.cpp
@@ -0,0 +1,228 @@
+#include "Olm.hpp"
+
+#include "Cache.h"
+#include "Logging.hpp"
+
+using namespace mtx::crypto;
+
+namespace {
+auto client_ = std::make_unique<mtx::crypto::OlmClient>();
+}
+
+namespace olm {
+
+mtx::crypto::OlmClient *
+client()
+{
+ return client_.get();
+}
+
+void
+handle_to_device_messages(const std::vector<nlohmann::json> &msgs)
+{
+ if (msgs.empty())
+ return;
+
+ nhlog::crypto()->info("received {} to_device messages", msgs.size());
+
+ for (const auto &msg : msgs) {
+ try {
+ OlmMessage olm_msg = msg;
+ handle_olm_message(std::move(olm_msg));
+ } catch (const nlohmann::json::exception &e) {
+ nhlog::crypto()->warn(
+ "parsing error for olm message: {} {}", e.what(), msg.dump(2));
+ } catch (const std::invalid_argument &e) {
+ nhlog::crypto()->warn(
+ "validation error for olm message: {} {}", e.what(), msg.dump(2));
+ }
+ }
+}
+
+void
+handle_olm_message(const OlmMessage &msg)
+{
+ nhlog::crypto()->info("sender : {}", msg.sender);
+ nhlog::crypto()->info("sender_key: {}", msg.sender_key);
+
+ const auto my_key = olm::client()->identity_keys().curve25519;
+
+ for (const auto &cipher : msg.ciphertext) {
+ // We skip messages not meant for the current device.
+ if (cipher.first != my_key)
+ continue;
+
+ const auto type = cipher.second.type;
+ nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE");
+
+ auto payload = try_olm_decryption(msg.sender_key, cipher.second);
+
+ if (payload) {
+ nhlog::crypto()->info("decrypted olm payload: {}", payload.value().dump(2));
+ create_inbound_megolm_session(msg.sender, msg.sender_key, payload.value());
+ return;
+ }
+
+ // Not a PRE_KEY message
+ if (cipher.second.type != 0) {
+ // TODO: log that it should have matched something
+ return;
+ }
+
+ handle_pre_key_olm_message(msg.sender, msg.sender_key, cipher.second);
+ }
+}
+
+void
+handle_pre_key_olm_message(const std::string &sender,
+ const std::string &sender_key,
+ const OlmCipherContent &content)
+{
+ nhlog::crypto()->info("opening olm session with {}", sender);
+
+ OlmSessionPtr inbound_session = nullptr;
+ try {
+ inbound_session = olm::client()->create_inbound_session(content.body);
+
+ // We also remove the one time key used to establish that
+ // session so we'll have to update our copy of the account object.
+ cache::client()->saveOlmAccount(olm::client()->save("secret"));
+ } catch (const olm_exception &e) {
+ nhlog::crypto()->critical(
+ "failed to create inbound session with {}: {}", sender, e.what());
+ return;
+ }
+
+ if (!matches_inbound_session_from(inbound_session.get(), sender_key, content.body)) {
+ nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})",
+ sender);
+ return;
+ }
+
+ mtx::crypto::BinaryBuf output;
+ try {
+ output =
+ olm::client()->decrypt_message(inbound_session.get(), content.type, content.body);
+ } catch (const olm_exception &e) {
+ nhlog::crypto()->critical(
+ "failed to decrypt olm message {}: {}", content.body, e.what());
+ return;
+ }
+
+ auto plaintext = json::parse(std::string((char *)output.data(), output.size()));
+ nhlog::crypto()->info("decrypted message: \n {}", plaintext.dump(2));
+
+ try {
+ cache::client()->saveOlmSession(sender_key, std::move(inbound_session));
+ } catch (const lmdb::error &e) {
+ nhlog::db()->warn(
+ "failed to save inbound olm session from {}: {}", sender, e.what());
+ }
+
+ create_inbound_megolm_session(sender, sender_key, plaintext);
+}
+
+mtx::events::msg::Encrypted
+encrypt_group_message(const std::string &room_id,
+ const std::string &device_id,
+ const std::string &body)
+{
+ using namespace mtx::events;
+
+ // Always chech before for existence.
+ auto res = cache::client()->getOutboundMegolmSession(room_id);
+ auto payload = olm::client()->encrypt_group_message(res.session, body);
+
+ // Prepare the m.room.encrypted event.
+ msg::Encrypted data;
+ data.ciphertext = std::string((char *)payload.data(), payload.size());
+ data.sender_key = olm::client()->identity_keys().curve25519;
+ data.session_id = res.data.session_id;
+ data.device_id = device_id;
+
+ auto message_index = olm_outbound_group_session_message_index(res.session);
+ nhlog::crypto()->info("next message_index {}", message_index);
+
+ // We need to re-pickle the session after we send a message to save the new message_index.
+ cache::client()->updateOutboundMegolmSession(room_id, message_index);
+
+ return data;
+}
+
+boost::optional<json>
+try_olm_decryption(const std::string &sender_key, const OlmCipherContent &msg)
+{
+ auto session_ids = cache::client()->getOlmSessions(sender_key);
+
+ for (const auto &id : session_ids) {
+ auto session = cache::client()->getOlmSession(sender_key, id);
+
+ if (!session)
+ continue;
+
+ mtx::crypto::BinaryBuf text;
+
+ try {
+ text = olm::client()->decrypt_message(session->get(), msg.type, msg.body);
+ cache::client()->saveOlmSession(id, std::move(session.value()));
+
+ } catch (const olm_exception &e) {
+ nhlog::crypto()->info("failed to decrypt olm message ({}, {}) with {}: {}",
+ msg.type,
+ sender_key,
+ id,
+ e.what());
+ continue;
+ } catch (const lmdb::error &e) {
+ nhlog::crypto()->critical("failed to save session: {}", e.what());
+ return {};
+ }
+
+ try {
+ return json::parse(std::string((char *)text.data(), text.size()));
+ } catch (const json::exception &e) {
+ nhlog::crypto()->critical("failed to parse the decrypted session msg: {}",
+ e.what());
+ }
+ }
+
+ return {};
+}
+
+void
+create_inbound_megolm_session(const std::string &sender,
+ const std::string &sender_key,
+ const nlohmann::json &payload)
+{
+ std::string room_id, session_id, session_key;
+
+ try {
+ room_id = payload.at("content").at("room_id");
+ session_id = payload.at("content").at("session_id");
+ session_key = payload.at("content").at("session_key");
+ } catch (const nlohmann::json::exception &e) {
+ nhlog::crypto()->critical(
+ "failed to parse plaintext olm message: {} {}", e.what(), payload.dump(2));
+ return;
+ }
+
+ MegolmSessionIndex index;
+ index.room_id = room_id;
+ index.session_id = session_id;
+ index.sender_key = sender_key;
+
+ try {
+ auto megolm_session = olm::client()->init_inbound_group_session(session_key);
+ cache::client()->saveInboundMegolmSession(index, std::move(megolm_session));
+ } catch (const lmdb::error &e) {
+ nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
+ return;
+ } catch (const olm_exception &e) {
+ nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what());
+ return;
+ }
+
+ nhlog::crypto()->info("established inbound megolm session ({}, {})", room_id, sender);
+}
+
+} // namespace olm
diff --git a/src/RegisterPage.cc b/src/RegisterPage.cc
index 7d80b727..db52e101 100644
--- a/src/RegisterPage.cc
+++ b/src/RegisterPage.cc
@@ -20,6 +20,7 @@
#include "Config.h"
#include "FlatButton.h"
+#include "Logging.hpp"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "RaisedButton.h"
@@ -125,35 +126,53 @@ RegisterPage::RegisterPage(QWidget *parent)
connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
- connect(http::client(),
- SIGNAL(registerError(const QString &)),
- this,
- SLOT(registerError(const QString &)));
- connect(http::client(),
- &MatrixClient::registrationFlow,
- this,
- [this](const QString &user,
- const QString &pass,
- const QString &server,
- const QString &session) {
- emit errorOccurred();
+ connect(this, &RegisterPage::registerErrorCb, this, &RegisterPage::registerError);
+ connect(
+ this,
+ &RegisterPage::registrationFlow,
+ this,
+ [this](const std::string &user, const std::string &pass, const std::string &session) {
+ emit errorOccurred();
- if (!captchaDialog_) {
- captchaDialog_ =
- std::make_shared<dialogs::ReCaptcha>(server, session, this);
- connect(captchaDialog_.get(),
- &dialogs::ReCaptcha::closing,
- this,
- [this, user, pass, server, session]() {
- captchaDialog_->close();
- emit registering();
- http::client()->registerUser(
- user, pass, server, session);
- });
- }
+ if (!captchaDialog_) {
+ captchaDialog_ = std::make_shared<dialogs::ReCaptcha>(
+ QString::fromStdString(session), this);
+ connect(
+ captchaDialog_.get(),
+ &dialogs::ReCaptcha::closing,
+ this,
+ [this, user, pass, session]() {
+ captchaDialog_->close();
+ emit registering();
- QTimer::singleShot(1000, this, [this]() { captchaDialog_->show(); });
- });
+ http::v2::client()->flow_response(
+ user,
+ pass,
+ session,
+ "m.login.recaptcha",
+ [this](const mtx::responses::Register &res,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn(
+ "failed to retrieve registration flows: {}",
+ err->matrix_error.error);
+ emit errorOccurred();
+ emit registerErrorCb(QString::fromStdString(
+ err->matrix_error.error));
+ return;
+ }
+
+ http::v2::client()->set_user(res.user_id);
+ http::v2::client()->set_access_token(
+ res.access_token);
+
+ emit registerOk();
+ });
+ });
+ }
+
+ QTimer::singleShot(1000, this, [this]() { captchaDialog_->show(); });
+ });
setLayout(top_layout_);
}
@@ -185,11 +204,56 @@ RegisterPage::onRegisterButtonClicked()
} else if (!server_input_->hasAcceptableInput()) {
registerError(tr("Invalid server name"));
} else {
- QString username = username_input_->text();
- QString password = password_input_->text();
- QString server = server_input_->text();
+ auto username = username_input_->text().toStdString();
+ auto password = password_input_->text().toStdString();
+ auto server = server_input_->text().toStdString();
+
+ http::v2::client()->set_server(server);
+ http::v2::client()->registration(
+ username,
+ password,
+ [this, username, password](const mtx::responses::Register &res,
+ mtx::http::RequestErr err) {
+ if (!err) {
+ http::v2::client()->set_user(res.user_id);
+ http::v2::client()->set_access_token(res.access_token);
+
+ emit registerOk();
+ return;
+ }
+
+ // The server requires registration flows.
+ if (err->status_code == boost::beast::http::status::unauthorized) {
+ http::v2::client()->flow_register(
+ username,
+ password,
+ [this, username, password](
+ const mtx::responses::RegistrationFlows &res,
+ mtx::http::RequestErr err) {
+ if (res.session.empty() && err) {
+ nhlog::net()->warn(
+ "failed to retrieve registration flows: ({}) "
+ "{}",
+ static_cast<int>(err->status_code),
+ err->matrix_error.error);
+ emit errorOccurred();
+ emit registerErrorCb(QString::fromStdString(
+ err->matrix_error.error));
+ return;
+ }
+
+ emit registrationFlow(username, password, res.session);
+ });
+ return;
+ }
+
+ nhlog::net()->warn("failed to register: status_code ({})",
+ static_cast<int>(err->status_code));
+
+ emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
+ emit errorOccurred();
+ });
- http::client()->registerUser(username, password, server);
emit registering();
}
}
diff --git a/src/RoomList.cc b/src/RoomList.cc
index e7c5ef30..b5bcdad6 100644
--- a/src/RoomList.cc
+++ b/src/RoomList.cc
@@ -16,11 +16,11 @@
*/
#include <QBuffer>
-#include <QDebug>
#include <QObject>
#include <QTimer>
#include "Cache.h"
+#include "Logging.hpp"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "OverlayModal.h"
@@ -55,18 +55,7 @@ RoomList::RoomList(QSharedPointer<UserSettings> userSettings, QWidget *parent)
scrollArea_->setWidget(scrollAreaContents_);
topLayout_->addWidget(scrollArea_);
- connect(http::client(),
- &MatrixClient::roomAvatarRetrieved,
- this,
- [this](const QString &room_id,
- const QPixmap &img,
- const QString &url,
- const QByteArray &data) {
- if (cache::client())
- cache::client()->saveImage(url, data);
-
- updateRoomAvatar(room_id, img);
- });
+ connect(this, &RoomList::updateRoomAvatarCb, this, &RoomList::updateRoomAvatar);
}
void
@@ -101,7 +90,28 @@ RoomList::updateAvatar(const QString &room_id, const QString &url)
savedImgData = cache::client()->image(url);
if (savedImgData.isEmpty()) {
- http::client()->fetchRoomAvatar(room_id, url);
+ mtx::http::ThumbOpts opts;
+ opts.mxc_url = url.toStdString();
+ http::v2::client()->get_thumbnail(
+ opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn(
+ "failed to download room avatar: {} {} {}",
+ opts.mxc_url,
+ mtx::errors::to_string(err->matrix_error.errcode),
+ err->matrix_error.error);
+ return;
+ }
+
+ if (cache::client())
+ cache::client()->saveImage(opts.mxc_url, res);
+
+ auto data = QByteArray(res.data(), res.size());
+ QPixmap pixmap;
+ pixmap.loadFromData(data);
+
+ emit updateRoomAvatarCb(room_id, pixmap);
+ });
} else {
QPixmap img;
img.loadFromData(savedImgData);
@@ -131,7 +141,8 @@ void
RoomList::updateUnreadMessageCount(const QString &roomid, int count)
{
if (!roomExists(roomid)) {
- qWarning() << "UpdateUnreadMessageCount: Unknown roomid";
+ nhlog::ui()->warn("updateUnreadMessageCount: unknown room_id {}",
+ roomid.toStdString());
return;
}
@@ -156,7 +167,7 @@ RoomList::calculateUnreadMessageCount()
void
RoomList::initialize(const QMap<QString, RoomInfo> &info)
{
- qDebug() << "initialize room list";
+ nhlog::ui()->info("initialize room list");
rooms_.clear();
@@ -209,7 +220,7 @@ RoomList::highlightSelectedRoom(const QString &room_id)
emit roomChanged(room_id);
if (!roomExists(room_id)) {
- qDebug() << "RoomList: clicked unknown roomid";
+ nhlog::ui()->warn("roomlist: clicked unknown room_id");
return;
}
@@ -232,7 +243,8 @@ void
RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img)
{
if (!roomExists(roomid)) {
- qWarning() << "Avatar update on non existent room" << roomid;
+ nhlog::ui()->warn("avatar update on non-existent room_id: {}",
+ roomid.toStdString());
return;
}
@@ -246,7 +258,9 @@ void
RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info)
{
if (!roomExists(roomid)) {
- qWarning() << "Description update on non existent room" << roomid << info.body;
+ nhlog::ui()->warn("description update on non-existent room_id: {}, {}",
+ roomid.toStdString(),
+ info.body.toStdString());
return;
}
@@ -314,7 +328,7 @@ RoomList::closeJoinRoomDialog(bool isJoining, QString roomAlias)
joinRoomModal_->hide();
if (isJoining)
- http::client()->joinRoom(roomAlias);
+ emit joinRoom(roomAlias);
}
void
diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc
index f3753971..acb33fa7 100644
--- a/src/TextInputWidget.cc
+++ b/src/TextInputWidget.cc
@@ -71,8 +71,6 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
this,
&FilteredTextEdit::uploadData);
- qRegisterMetaType<SearchResult>();
- qRegisterMetaType<QVector<SearchResult>>();
connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults);
connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) {
popup_.hide();
diff --git a/src/dialogs/PreviewUploadOverlay.cc b/src/dialogs/PreviewUploadOverlay.cc
index 3c44e911..e01d2b17 100644
--- a/src/dialogs/PreviewUploadOverlay.cc
+++ b/src/dialogs/PreviewUploadOverlay.cc
@@ -17,7 +17,6 @@
#include <QApplication>
#include <QBuffer>
-#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QHBoxLayout>
@@ -25,14 +24,15 @@
#include <QVBoxLayout>
#include "Config.h"
+#include "Logging.hpp"
#include "Utils.h"
#include "dialogs/PreviewUploadOverlay.h"
using namespace dialogs;
-static constexpr const char *DEFAULT = "Upload %1?";
-static constexpr const char *ERROR = "Failed to load image type '%1'. Continue upload?";
+constexpr const char *DEFAULT = "Upload %1?";
+constexpr const char *ERR_MSG = "Failed to load image type '%1'. Continue upload?";
PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent)
: QWidget{parent}
@@ -105,7 +105,7 @@ PreviewUploadOverlay::setLabels(const QString &type, const QString &mime, uint64
{
if (mediaType_ == "image") {
if (!image_.loadFromData(data_)) {
- titleLabel_.setText(QString{tr(ERROR)}.arg(type));
+ titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type));
} else {
titleLabel_.setText(QString{tr(DEFAULT)}.arg(mediaType_));
}
@@ -142,8 +142,9 @@ PreviewUploadOverlay::setPreview(const QString &path)
QFile file{path};
if (!file.open(QIODevice::ReadOnly)) {
- qWarning() << "Failed to open file from:" << path;
- qWarning() << "Reason:" << file.errorString();
+ nhlog::ui()->warn("Failed to open file ({}): {}",
+ path.toStdString(),
+ file.errorString().toStdString());
close();
return;
}
@@ -152,7 +153,7 @@ PreviewUploadOverlay::setPreview(const QString &path)
auto mime = db.mimeTypeForFileNameAndData(path, &file);
if ((data_ = file.readAll()).isEmpty()) {
- qWarning() << "Failed to read media:" << file.errorString();
+ nhlog::ui()->warn("Failed to read media: {}", file.errorString().toStdString());
close();
return;
}
diff --git a/src/dialogs/ReCaptcha.cpp b/src/dialogs/ReCaptcha.cpp
index ba487cea..6b1143b5 100644
--- a/src/dialogs/ReCaptcha.cpp
+++ b/src/dialogs/ReCaptcha.cpp
@@ -6,6 +6,7 @@
#include "Config.h"
#include "FlatButton.h"
+#include "MatrixClient.h"
#include "RaisedButton.h"
#include "Theme.h"
@@ -13,7 +14,7 @@
using namespace dialogs;
-ReCaptcha::ReCaptcha(const QString &server, const QString &session, QWidget *parent)
+ReCaptcha::ReCaptcha(const QString &session, QWidget *parent)
: QWidget(parent)
{
setAutoFillBackground(true);
@@ -51,12 +52,12 @@ ReCaptcha::ReCaptcha(const QString &server, const QString &session, QWidget *par
layout->addWidget(label);
layout->addLayout(buttonLayout);
- connect(openCaptchaBtn_, &QPushButton::clicked, [server, session]() {
- const auto url =
- QString(
- "https://%1/_matrix/client/r0/auth/m.login.recaptcha/fallback/web?session=%2")
- .arg(server)
- .arg(session);
+ connect(openCaptchaBtn_, &QPushButton::clicked, [session]() {
+ const auto url = QString("https://%1:%2/_matrix/client/r0/auth/m.login.recaptcha/"
+ "fallback/web?session=%3")
+ .arg(QString::fromStdString(http::v2::client()->server()))
+ .arg(http::v2::client()->port())
+ .arg(session);
QDesktopServices::openUrl(url);
});
diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp
index 4d2f304b..74d08478 100644
--- a/src/dialogs/RoomSettings.cpp
+++ b/src/dialogs/RoomSettings.cpp
@@ -1,16 +1,20 @@
#include "Avatar.h"
+#include "ChatPage.h"
#include "Config.h"
#include "FlatButton.h"
+#include "Logging.hpp"
#include "MatrixClient.h"
#include "Painter.h"
#include "TextField.h"
#include "Theme.h"
#include "Utils.h"
#include "dialogs/RoomSettings.hpp"
+#include "ui/ToggleButton.h"
#include <QApplication>
#include <QComboBox>
#include <QLabel>
+#include <QMessageBox>
#include <QPainter>
#include <QPixmap>
#include <QSettings>
@@ -67,6 +71,20 @@ EditModal::EditModal(const QString &roomId, QWidget *parent)
labelLayout->addWidget(errorField_);
layout->addLayout(labelLayout);
+ connect(this, &EditModal::stateEventErrorCb, this, [this](const QString &msg) {
+ errorField_->setText(msg);
+ errorField_->show();
+ });
+ connect(this, &EditModal::nameEventSentCb, this, [this](const QString &newName) {
+ errorField_->hide();
+ emit nameChanged(newName);
+ close();
+ });
+ connect(this, &EditModal::topicEventSentCb, this, [this]() {
+ errorField_->hide();
+ close();
+ });
+
connect(applyBtn_, &QPushButton::clicked, [this]() {
// Check if the values are changed from the originals.
auto newName = nameInput_->text().trimmed();
@@ -85,53 +103,37 @@ EditModal::EditModal(const QString &roomId, QWidget *parent)
state::Name body;
body.name = newName.toStdString();
- auto proxy =
- http::client()->sendStateEvent<state::Name, EventType::RoomName>(body,
- roomId_);
- connect(proxy.get(),
- &StateEventProxy::stateEventSent,
- this,
- [this, proxy, newName]() {
- Q_UNUSED(proxy);
- errorField_->hide();
- emit nameChanged(newName);
- close();
- });
+ http::v2::client()->send_state_event<state::Name, EventType::RoomName>(
+ roomId_.toStdString(),
+ body,
+ [this, newName](const mtx::responses::EventId &,
+ mtx::http::RequestErr err) {
+ if (err) {
+ emit stateEventErrorCb(
+ QString::fromStdString(err->matrix_error.error));
+ return;
+ }
- connect(proxy.get(),
- &StateEventProxy::stateEventError,
- this,
- [this, proxy, newName](const QString &msg) {
- Q_UNUSED(proxy);
- errorField_->setText(msg);
- errorField_->show();
- });
+ emit nameEventSentCb(newName);
+ });
}
if (newTopic != initialTopic_ && !newTopic.isEmpty()) {
state::Topic body;
body.topic = newTopic.toStdString();
- auto proxy =
- http::client()->sendStateEvent<state::Topic, EventType::RoomTopic>(
- body, roomId_);
- connect(proxy.get(),
- &StateEventProxy::stateEventSent,
- this,
- [this, proxy, newTopic]() {
- Q_UNUSED(proxy);
- errorField_->hide();
- close();
- });
+ http::v2::client()->send_state_event<state::Topic, EventType::RoomTopic>(
+ roomId_.toStdString(),
+ body,
+ [this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+ if (err) {
+ emit stateEventErrorCb(
+ QString::fromStdString(err->matrix_error.error));
+ return;
+ }
- connect(proxy.get(),
- &StateEventProxy::stateEventError,
- this,
- [this, proxy, newTopic](const QString &msg) {
- Q_UNUSED(proxy);
- errorField_->setText(msg);
- errorField_->show();
- });
+ emit topicEventSentCb();
+ });
}
});
connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close);
@@ -190,8 +192,8 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
layout->setSpacing(15);
layout->setMargin(20);
- saveBtn_ = new FlatButton("SAVE", this);
- saveBtn_->setFontSize(conf::btn::fontSize);
+ okBtn_ = new FlatButton(tr("OK"), this);
+ okBtn_->setFontSize(conf::btn::fontSize);
cancelBtn_ = new FlatButton(tr("CANCEL"), this);
cancelBtn_->setFontSize(conf::btn::fontSize);
@@ -199,7 +201,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
btnLayout->setSpacing(0);
btnLayout->setMargin(0);
btnLayout->addStretch(1);
- btnLayout->addWidget(saveBtn_);
+ btnLayout->addWidget(okBtn_);
btnLayout->addWidget(cancelBtn_);
auto notifOptionLayout_ = new QHBoxLayout;
@@ -238,6 +240,61 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
accessOptionLayout->addWidget(accessLabel);
accessOptionLayout->addWidget(accessCombo);
+ auto encryptionOptionLayout = new QHBoxLayout;
+ encryptionOptionLayout->setMargin(SettingsMargin);
+ auto encryptionLabel = new QLabel(tr("Encryption"), this);
+ encryptionLabel->setStyleSheet("font-size: 15px;");
+ encryptionToggle_ = new Toggle(this);
+ connect(encryptionToggle_, &Toggle::toggled, this, [this](bool isOn) {
+ if (isOn)
+ return;
+
+ QFont font;
+ font.setPixelSize(conf::fontSize);
+
+ QMessageBox msgBox;
+ msgBox.setIcon(QMessageBox::Question);
+ msgBox.setFont(font);
+ msgBox.setWindowTitle(tr("End-to-End Encryption"));
+ msgBox.setText(tr(
+ "Encryption is currently experimental and things might break unexpectedly. <br>"
+ "Please take note that it can't be disabled afterwards."));
+ msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
+ msgBox.setDefaultButton(QMessageBox::Save);
+ int ret = msgBox.exec();
+
+ switch (ret) {
+ case QMessageBox::Ok: {
+ encryptionToggle_->setState(false);
+ encryptionToggle_->setEnabled(false);
+ enableEncryption();
+ break;
+ }
+ default: {
+ encryptionToggle_->setState(true);
+ encryptionToggle_->setEnabled(true);
+ break;
+ }
+ }
+ });
+
+ encryptionOptionLayout->addWidget(encryptionLabel);
+ encryptionOptionLayout->addWidget(encryptionToggle_, 0, Qt::AlignBottom | Qt::AlignRight);
+
+ // Disable encryption button.
+ if (usesEncryption_) {
+ encryptionToggle_->setState(false);
+ encryptionToggle_->setEnabled(false);
+ } else {
+ encryptionToggle_->setState(true);
+ }
+
+ // Hide encryption option for public rooms.
+ if (!usesEncryption_ && (info_.join_rule == JoinRule::Public)) {
+ encryptionToggle_->hide();
+ encryptionLabel->hide();
+ }
+
QFont font;
font.setPixelSize(18);
font.setWeight(70);
@@ -257,10 +314,18 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
layout->addLayout(editLayout_);
layout->addLayout(notifOptionLayout_);
layout->addLayout(accessOptionLayout);
+ layout->addLayout(encryptionOptionLayout);
layout->addLayout(btnLayout);
connect(cancelBtn_, &QPushButton::clicked, this, &RoomSettings::closing);
- connect(saveBtn_, &QPushButton::clicked, this, &RoomSettings::saveSettings);
+ connect(okBtn_, &QPushButton::clicked, this, &RoomSettings::saveSettings);
+
+ connect(this, &RoomSettings::enableEncryptionError, this, [this](const QString &msg) {
+ encryptionToggle_->setState(true);
+ encryptionToggle_->setEnabled(true);
+
+ emit ChatPage::instance()->showNotification(msg);
+ });
}
void
@@ -273,7 +338,7 @@ RoomSettings::setupEditButton()
hasEditRights_ = cache::client()->hasEnoughPowerLevel(
{EventType::RoomName, EventType::RoomTopic}, room_id_.toStdString(), userId);
} catch (const lmdb::error &e) {
- qWarning() << "lmdb error" << e.what();
+ nhlog::db()->warn("lmdb error: {}", e.what());
}
constexpr int buttonSize = 36;
@@ -310,10 +375,12 @@ void
RoomSettings::retrieveRoomInfo()
{
try {
- info_ = cache::client()->singleRoomInfo(room_id_.toStdString());
+ usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString());
+ info_ = cache::client()->singleRoomInfo(room_id_.toStdString());
setAvatar(QImage::fromData(cache::client()->image(info_.avatar_url)));
} catch (const lmdb::error &e) {
- qWarning() << "failed to retrieve room info from cache" << room_id_;
+ nhlog::db()->warn("failed to retrieve room info from cache: {}",
+ room_id_.toStdString());
}
}
@@ -342,6 +409,28 @@ RoomSettings::saveSettings()
}
void
+RoomSettings::enableEncryption()
+{
+ const auto room_id = room_id_.toStdString();
+ http::v2::client()->enable_encryption(
+ room_id, [room_id, this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+ if (err) {
+ int status_code = static_cast<int>(err->status_code);
+ nhlog::net()->warn("failed to enable encryption in room ({}): {} {}",
+ room_id,
+ err->matrix_error.error,
+ status_code);
+ emit enableEncryptionError(
+ tr("Failed to enable encryption: %1")
+ .arg(QString::fromStdString(err->matrix_error.error)));
+ return;
+ }
+
+ nhlog::net()->info("enabled encryption on room ({})", room_id);
+ });
+}
+
+void
RoomSettings::paintEvent(QPaintEvent *)
{
QStyleOption opt;
diff --git a/src/main.cc b/src/main.cc
index bd3a212c..327ec587 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -17,20 +17,23 @@
#include <QApplication>
#include <QDesktopWidget>
+#include <QDir>
#include <QFile>
#include <QFontDatabase>
#include <QLabel>
#include <QLayout>
#include <QLibraryInfo>
-#include <QNetworkProxy>
#include <QPalette>
#include <QPoint>
#include <QPushButton>
#include <QSettings>
+#include <QStandardPaths>
#include <QTranslator>
#include "Config.h"
+#include "Logging.hpp"
#include "MainWindow.h"
+#include "MatrixClient.h"
#include "RaisedButton.h"
#include "RunGuard.h"
#include "version.hpp"
@@ -47,28 +50,13 @@ screenCenter(int width, int height)
}
void
-setupProxy()
+createCacheDirectory()
{
- QSettings settings;
+ auto dir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
- /**
- To set up a SOCKS proxy:
- [user]
- proxy\socks\host=<>
- proxy\socks\port=<>
- proxy\socks\user=<>
- proxy\socks\password=<>
- **/
- if (settings.contains("user/proxy/socks/host")) {
- QNetworkProxy proxy;
- proxy.setType(QNetworkProxy::Socks5Proxy);
- proxy.setHostName(settings.value("user/proxy/socks/host").toString());
- proxy.setPort(settings.value("user/proxy/socks/port").toInt());
- if (settings.contains("user/proxy/socks/user"))
- proxy.setUser(settings.value("user/proxy/socks/user").toString());
- if (settings.contains("user/proxy/socks/password"))
- proxy.setPassword(settings.value("user/proxy/socks/password").toString());
- QNetworkProxy::setApplicationProxy(proxy);
+ if (!QDir().mkpath(dir)) {
+ throw std::runtime_error(
+ ("Unable to create state directory:" + dir).toStdString().c_str());
}
}
@@ -133,7 +121,19 @@ main(int argc, char *argv[])
QFontDatabase::addApplicationFont(":/fonts/fonts/EmojiOne/emojione-android.ttf");
app.setWindowIcon(QIcon(":/logos/nheko.png"));
- qSetMessagePattern("%{time process}: [%{type}] - %{message}");
+
+ http::init();
+
+ createCacheDirectory();
+
+ try {
+ nhlog::init(QString("%1/nheko.log")
+ .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
+ .toStdString());
+ } catch (const spdlog::spdlog_ex &ex) {
+ std::cout << "Log initialization failed: " << ex.what() << std::endl;
+ std::exit(1);
+ }
QSettings settings;
@@ -154,8 +154,6 @@ main(int argc, char *argv[])
appTranslator.load("nheko_" + lang, ":/translations");
app.installTranslator(&appTranslator);
- setupProxy();
-
MainWindow w;
// Move the MainWindow to the center
@@ -165,7 +163,16 @@ main(int argc, char *argv[])
!settings.value("user/window/tray", true).toBool())
w.show();
- QObject::connect(&app, &QApplication::aboutToQuit, &w, &MainWindow::saveCurrentWindowSize);
+ QObject::connect(&app, &QApplication::aboutToQuit, &w, [&w]() {
+ w.saveCurrentWindowSize();
+ if (http::v2::client() != nullptr) {
+ nhlog::net()->info("shutting down all I/O threads & open connections");
+ http::v2::client()->shutdown();
+ http::v2::client()->close(true);
+ }
+ });
+
+ nhlog::ui()->info("starting nheko {}", nheko::version);
return app.exec();
}
diff --git a/src/timeline/TimelineItem.cc b/src/timeline/TimelineItem.cc
index 250373e4..c104801d 100644
--- a/src/timeline/TimelineItem.cc
+++ b/src/timeline/TimelineItem.cc
@@ -23,6 +23,7 @@
#include "Avatar.h"
#include "ChatPage.h"
#include "Config.h"
+#include "Logging.hpp"
#include "timeline/TimelineItem.h"
#include "timeline/widgets/AudioItem.h"
@@ -62,9 +63,27 @@ TimelineItem::init()
ChatPage::instance()->showReadReceipts(event_id_);
});
+ connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) {
+ emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id);
+ });
+ connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) {
+ emit ChatPage::instance()->showNotification(msg);
+ });
connect(redactMsg_, &QAction::triggered, this, [this]() {
if (!event_id_.isEmpty())
- http::client()->redactEvent(room_id_, event_id_);
+ http::v2::client()->redact_event(
+ room_id_.toStdString(),
+ event_id_.toStdString(),
+ [this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+ if (err) {
+ emit redactionFailed(tr("Message redaction failed: %1")
+ .arg(QString::fromStdString(
+ err->matrix_error.error)));
+ return;
+ }
+
+ emit eventRedacted(event_id_);
+ });
});
connect(markAsRead_, &QAction::triggered, this, [this]() { sendReadReceipt(); });
@@ -413,6 +432,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text>
void
TimelineItem::markReceived()
{
+ isReceived_ = true;
checkmark_->setText(CHECKMARK);
checkmark_->setAlignment(Qt::AlignTop);
@@ -635,3 +655,19 @@ TimelineItem::addAvatar()
AvatarProvider::resolve(
room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); });
}
+
+void
+TimelineItem::sendReadReceipt() const
+{
+ if (!event_id_.isEmpty())
+ http::v2::client()->read_event(room_id_.toStdString(),
+ event_id_.toStdString(),
+ [this](mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn(
+ "failed to read_event ({}, {})",
+ room_id_.toStdString(),
+ event_id_.toStdString());
+ }
+ });
+}
diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc
index 71058d74..e437439e 100644
--- a/src/timeline/TimelineView.cc
+++ b/src/timeline/TimelineView.cc
@@ -23,6 +23,8 @@
#include "ChatPage.h"
#include "Config.h"
#include "FloatingButton.h"
+#include "Logging.hpp"
+#include "Olm.hpp"
#include "UserSettingsPage.h"
#include "Utils.h"
@@ -100,7 +102,7 @@ TimelineView::TimelineView(const QString &room_id, QWidget *parent)
, room_id_{room_id}
{
init();
- http::client()->messages(room_id_, "");
+ getMessages();
}
void
@@ -140,7 +142,7 @@ TimelineView::fetchHistory()
return;
isPaginationInProgress_ = true;
- http::client()->messages(room_id_, prev_batch_token_);
+ getMessages();
paginationTimer_->start(5000);
return;
@@ -189,18 +191,13 @@ TimelineView::sliderMoved(int position)
isPaginationInProgress_ = true;
- // FIXME: Maybe move this to TimelineViewManager to remove the
- // extra calls?
- http::client()->messages(room_id_, prev_batch_token_);
+ getMessages();
}
}
void
-TimelineView::addBackwardsEvents(const QString &room_id, const mtx::responses::Messages &msgs)
+TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs)
{
- if (room_id_ != room_id)
- return;
-
// We've reached the start of the timline and there're no more messages.
if ((msgs.end == msgs.start) && msgs.chunk.size() == 0) {
isTimelineFinished = true;
@@ -239,19 +236,19 @@ TimelineItem *
TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event,
TimelineDirection direction)
{
- namespace msg = mtx::events::msg;
- using AudioEvent = mtx::events::RoomEvent<msg::Audio>;
- using EmoteEvent = mtx::events::RoomEvent<msg::Emote>;
- using FileEvent = mtx::events::RoomEvent<msg::File>;
- using ImageEvent = mtx::events::RoomEvent<msg::Image>;
- using NoticeEvent = mtx::events::RoomEvent<msg::Notice>;
- using TextEvent = mtx::events::RoomEvent<msg::Text>;
- using VideoEvent = mtx::events::RoomEvent<msg::Video>;
+ using namespace mtx::events;
+
+ using AudioEvent = RoomEvent<msg::Audio>;
+ using EmoteEvent = RoomEvent<msg::Emote>;
+ using FileEvent = RoomEvent<msg::File>;
+ using ImageEvent = RoomEvent<msg::Image>;
+ using NoticeEvent = RoomEvent<msg::Notice>;
+ using TextEvent = RoomEvent<msg::Text>;
+ using VideoEvent = RoomEvent<msg::Video>;
- if (mpark::holds_alternative<mtx::events::RedactionEvent<msg::Redaction>>(event)) {
- auto redaction_event =
- mpark::get<mtx::events::RedactionEvent<msg::Redaction>>(event);
- const auto event_id = QString::fromStdString(redaction_event.redacts);
+ if (mpark::holds_alternative<RedactionEvent<msg::Redaction>>(event)) {
+ auto redaction_event = mpark::get<RedactionEvent<msg::Redaction>>(event);
+ const auto event_id = QString::fromStdString(redaction_event.redacts);
QTimer::singleShot(0, this, [event_id, this]() {
if (eventIds_.contains(event_id))
@@ -259,35 +256,96 @@ TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &
});
return nullptr;
- } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Audio>>(event)) {
- auto audio = mpark::get<mtx::events::RoomEvent<msg::Audio>>(event);
+ } else if (mpark::holds_alternative<RoomEvent<msg::Audio>>(event)) {
+ auto audio = mpark::get<RoomEvent<msg::Audio>>(event);
return processMessageEvent<AudioEvent, AudioItem>(audio, direction);
- } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Emote>>(event)) {
- auto emote = mpark::get<mtx::events::RoomEvent<msg::Emote>>(event);
+ } else if (mpark::holds_alternative<RoomEvent<msg::Emote>>(event)) {
+ auto emote = mpark::get<RoomEvent<msg::Emote>>(event);
return processMessageEvent<EmoteEvent>(emote, direction);
- } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::File>>(event)) {
- auto file = mpark::get<mtx::events::RoomEvent<msg::File>>(event);
+ } else if (mpark::holds_alternative<RoomEvent<msg::File>>(event)) {
+ auto file = mpark::get<RoomEvent<msg::File>>(event);
return processMessageEvent<FileEvent, FileItem>(file, direction);
- } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Image>>(event)) {
- auto image = mpark::get<mtx::events::RoomEvent<msg::Image>>(event);
+ } else if (mpark::holds_alternative<RoomEvent<msg::Image>>(event)) {
+ auto image = mpark::get<RoomEvent<msg::Image>>(event);
return processMessageEvent<ImageEvent, ImageItem>(image, direction);
- } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Notice>>(event)) {
- auto notice = mpark::get<mtx::events::RoomEvent<msg::Notice>>(event);
+ } else if (mpark::holds_alternative<RoomEvent<msg::Notice>>(event)) {
+ auto notice = mpark::get<RoomEvent<msg::Notice>>(event);
return processMessageEvent<NoticeEvent>(notice, direction);
- } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Text>>(event)) {
- auto text = mpark::get<mtx::events::RoomEvent<msg::Text>>(event);
+ } else if (mpark::holds_alternative<RoomEvent<msg::Text>>(event)) {
+ auto text = mpark::get<RoomEvent<msg::Text>>(event);
return processMessageEvent<TextEvent>(text, direction);
- } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Video>>(event)) {
- auto video = mpark::get<mtx::events::RoomEvent<msg::Video>>(event);
+ } else if (mpark::holds_alternative<RoomEvent<msg::Video>>(event)) {
+ auto video = mpark::get<RoomEvent<msg::Video>>(event);
return processMessageEvent<VideoEvent, VideoItem>(video, direction);
- } else if (mpark::holds_alternative<mtx::events::Sticker>(event)) {
- return processMessageEvent<mtx::events::Sticker, StickerItem>(
- mpark::get<mtx::events::Sticker>(event), direction);
+ } else if (mpark::holds_alternative<Sticker>(event)) {
+ return processMessageEvent<Sticker, StickerItem>(mpark::get<Sticker>(event),
+ direction);
+ } else if (mpark::holds_alternative<EncryptedEvent<msg::Encrypted>>(event)) {
+ auto decrypted =
+ parseEncryptedEvent(mpark::get<EncryptedEvent<msg::Encrypted>>(event));
+ return parseMessageEvent(decrypted, direction);
+ } else if (mpark::holds_alternative<StateEvent<state::Encryption>>(event)) {
+ try {
+ cache::client()->setEncryptedRoom(room_id_.toStdString());
+ } catch (const lmdb::error &e) {
+ nhlog::db()->critical("failed to save room {} as encrypted",
+ room_id_.toStdString());
+ }
}
return nullptr;
}
+TimelineEvent
+TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e)
+{
+ MegolmSessionIndex index;
+ index.room_id = room_id_.toStdString();
+ index.session_id = e.content.session_id;
+ index.sender_key = e.content.sender_key;
+
+ mtx::events::RoomEvent<mtx::events::msg::Text> dummy;
+ dummy.origin_server_ts = e.origin_server_ts;
+ dummy.event_id = e.event_id;
+ dummy.sender = e.sender;
+ dummy.content.body = "-- Encrypted Event (No keys found for decryption) --";
+
+ if (!cache::client()->inboundMegolmSessionExists(index)) {
+ nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
+ index.room_id,
+ index.session_id,
+ e.sender);
+ // TODO: request megolm session_id & session_key from the sender.
+ return dummy;
+ }
+
+ auto session = cache::client()->getInboundMegolmSession(index);
+ auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext);
+
+ const auto msg_str = std::string((char *)res.data.data(), res.data.size());
+
+ // Add missing fields for the event.
+ json body = json::parse(msg_str);
+ body["event_id"] = e.event_id;
+ body["sender"] = e.sender;
+ body["origin_server_ts"] = e.origin_server_ts;
+ body["unsigned"] = e.unsigned_data;
+
+ nhlog::crypto()->info("decrypted data: \n {}", body.dump(2));
+
+ json event_array = json::array();
+ event_array.push_back(body);
+
+ std::vector<TimelineEvent> events;
+ mtx::responses::utils::parse_timeline_events(event_array, events);
+
+ if (events.size() == 1)
+ return events.at(0);
+
+ dummy.content.body = "-- Encrypted Event (Unknown event type) --";
+ return dummy;
+}
+
void
TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events)
{
@@ -427,10 +485,10 @@ TimelineView::init()
paginationTimer_ = new QTimer(this);
connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory);
- connect(http::client(),
- &MatrixClient::messagesRetrieved,
- this,
- &TimelineView::addBackwardsEvents);
+ connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents);
+
+ connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage);
+ connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage);
connect(scroll_area_->verticalScrollBar(),
SIGNAL(valueChanged(int)),
@@ -443,6 +501,27 @@ TimelineView::init()
}
void
+TimelineView::getMessages()
+{
+ mtx::http::MessagesOpts opts;
+ opts.room_id = room_id_.toStdString();
+ opts.from = prev_batch_token_.toStdString();
+
+ http::v2::client()->messages(
+ opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->error("failed to call /messages ({}): {} - {}",
+ opts.room_id,
+ mtx::errors::to_string(err->matrix_error.errcode),
+ err->matrix_error.error);
+ return;
+ }
+
+ emit messagesRetrieved(std::move(res));
+ });
+}
+
+void
TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction)
{
if (direction == TimelineDirection::Bottom)
@@ -513,8 +592,9 @@ TimelineView::addTimelineItem(TimelineItem *item, TimelineDirection direction)
}
void
-TimelineView::updatePendingMessage(int txn_id, QString event_id)
+TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id)
{
+ nhlog::ui()->info("[{}] message was received by the server", txn_id);
if (!pending_msgs_.isEmpty() &&
pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet
auto msg = pending_msgs_.dequeue();
@@ -522,11 +602,18 @@ TimelineView::updatePendingMessage(int txn_id, QString event_id)
if (msg.widget) {
msg.widget->setEventId(event_id);
- msg.widget->markReceived();
eventIds_[event_id] = msg.widget;
- }
- pending_sent_msgs_.append(msg);
+ // If the response comes after we have received the event from sync
+ // we've already marked the widget as received.
+ if (!msg.widget->isReceived()) {
+ msg.widget->markReceived();
+ pending_sent_msgs_.append(msg);
+ }
+ } else {
+ nhlog::ui()->warn("[{}] received message response for invalid widget",
+ txn_id);
+ }
}
sendNextPendingMessage();
@@ -540,16 +627,28 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body)
TimelineItem *view_item =
new TimelineItem(ty, local_user_, body, with_sender, room_id_, scroll_widget_);
+ PendingMessage message;
+ message.ty = ty;
+ message.txn_id = http::v2::client()->generate_txn_id();
+ message.body = body;
+ message.widget = view_item;
+
+ try {
+ message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString());
+ } catch (const lmdb::error &e) {
+ nhlog::db()->critical("failed to check encryption status of room {}", e.what());
+ view_item->deleteLater();
+
+ // TODO: Send a notification to the user.
+
+ return;
+ }
+
addTimelineItem(view_item);
lastMessageDirection_ = TimelineDirection::Bottom;
- QApplication::processEvents();
-
saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
-
- int txn_id = http::client()->incrementTransactionId();
- PendingMessage message(ty, txn_id, body, "", "", -1, "", view_item);
handleNewUserMessage(message);
}
@@ -567,19 +666,98 @@ TimelineView::sendNextPendingMessage()
if (pending_msgs_.size() == 0)
return;
+ using namespace mtx::events;
+
PendingMessage &m = pending_msgs_.head();
+
+ nhlog::ui()->info("[{}] sending next queued message", m.txn_id);
+
+ if (m.is_encrypted) {
+ prepareEncryptedMessage(std::move(m));
+ nhlog::ui()->info("[{}] sending encrypted event", m.txn_id);
+ return;
+ }
+
switch (m.ty) {
- case mtx::events::MessageType::Audio:
- case mtx::events::MessageType::Image:
- case mtx::events::MessageType::Video:
- case mtx::events::MessageType::File:
- // FIXME: Improve the API
- http::client()->sendRoomMessage(
- m.ty, m.txn_id, room_id_, m.filename, m.mime, m.media_size, m.body);
+ case mtx::events::MessageType::Audio: {
+ http::v2::client()->send_room_message<msg::Audio, EventType::RoomMessage>(
+ room_id_.toStdString(),
+ m.txn_id,
+ toRoomMessage<msg::Audio>(m),
+ std::bind(&TimelineView::sendRoomMessageHandler,
+ this,
+ m.txn_id,
+ std::placeholders::_1,
+ std::placeholders::_2));
+
+ break;
+ }
+ case mtx::events::MessageType::Image: {
+ http::v2::client()->send_room_message<msg::Image, EventType::RoomMessage>(
+ room_id_.toStdString(),
+ m.txn_id,
+ toRoomMessage<msg::Image>(m),
+ std::bind(&TimelineView::sendRoomMessageHandler,
+ this,
+ m.txn_id,
+ std::placeholders::_1,
+ std::placeholders::_2));
+
+ break;
+ }
+ case mtx::events::MessageType::Video: {
+ http::v2::client()->send_room_message<msg::Video, EventType::RoomMessage>(
+ room_id_.toStdString(),
+ m.txn_id,
+ toRoomMessage<msg::Video>(m),
+ std::bind(&TimelineView::sendRoomMessageHandler,
+ this,
+ m.txn_id,
+ std::placeholders::_1,
+ std::placeholders::_2));
+
+ break;
+ }
+ case mtx::events::MessageType::File: {
+ http::v2::client()->send_room_message<msg::File, EventType::RoomMessage>(
+ room_id_.toStdString(),
+ m.txn_id,
+ toRoomMessage<msg::File>(m),
+ std::bind(&TimelineView::sendRoomMessageHandler,
+ this,
+ m.txn_id,
+ std::placeholders::_1,
+ std::placeholders::_2));
+
+ break;
+ }
+ case mtx::events::MessageType::Text: {
+ http::v2::client()->send_room_message<msg::Text, EventType::RoomMessage>(
+ room_id_.toStdString(),
+ m.txn_id,
+ toRoomMessage<msg::Text>(m),
+ std::bind(&TimelineView::sendRoomMessageHandler,
+ this,
+ m.txn_id,
+ std::placeholders::_1,
+ std::placeholders::_2));
+
+ break;
+ }
+ case mtx::events::MessageType::Emote: {
+ http::v2::client()->send_room_message<msg::Emote, EventType::RoomMessage>(
+ room_id_.toStdString(),
+ m.txn_id,
+ toRoomMessage<msg::Emote>(m),
+ std::bind(&TimelineView::sendRoomMessageHandler,
+ this,
+ m.txn_id,
+ std::placeholders::_1,
+ std::placeholders::_2));
break;
+ }
default:
- http::client()->sendRoomMessage(
- m.ty, m.txn_id, room_id_, m.body, m.mime, m.media_size);
+ nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString());
break;
}
}
@@ -593,7 +771,7 @@ TimelineView::notifyForLastEvent()
if (lastTimelineItem)
emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage());
else
- qWarning() << "Cast to TimelineView failed" << room_id_;
+ nhlog::ui()->warn("cast to TimelineView failed: {}", room_id_.toStdString());
}
void
@@ -606,51 +784,51 @@ TimelineView::notifyForLastEvent(const TimelineEvent &event)
}
bool
-TimelineView::isPendingMessage(const QString &txnid,
+TimelineView::isPendingMessage(const std::string &txn_id,
const QString &sender,
const QString &local_userid)
{
if (sender != local_userid)
return false;
- auto match_txnid = [txnid](const auto &msg) -> bool {
- return QString::number(msg.txn_id) == txnid;
- };
+ auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; };
return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) ||
std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid);
}
void
-TimelineView::removePendingMessage(const QString &txnid)
+TimelineView::removePendingMessage(const std::string &txn_id)
{
- if (txnid.isEmpty())
+ if (txn_id.empty())
return;
for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) {
- if (QString::number(it->txn_id) == txnid) {
+ if (it->txn_id == txn_id) {
int index = std::distance(pending_sent_msgs_.begin(), it);
pending_sent_msgs_.removeAt(index);
if (pending_sent_msgs_.isEmpty())
sendNextPendingMessage();
- return;
+ nhlog::ui()->info("[{}] removed message with sync", txn_id);
}
}
for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) {
- if (QString::number(it->txn_id) == txnid) {
- int index = std::distance(pending_msgs_.begin(), it);
- pending_msgs_.removeAt(index);
+ if (it->txn_id == txn_id) {
+ if (it->widget)
+ it->widget->markReceived();
+
+ nhlog::ui()->info("[{}] received sync before message response", txn_id);
return;
}
}
}
void
-TimelineView::handleFailedMessage(int txnid)
+TimelineView::handleFailedMessage(const std::string &txn_id)
{
- Q_UNUSED(txnid);
+ Q_UNUSED(txn_id);
// Note: We do this even if the message has already been echoed.
QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage()));
}
@@ -673,7 +851,16 @@ TimelineView::readLastEvent() const
const auto eventId = getLastEventId();
if (!eventId.isEmpty())
- http::client()->readEvent(room_id_, eventId);
+ http::v2::client()->read_event(room_id_.toStdString(),
+ eventId.toStdString(),
+ [this, eventId](mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn(
+ "failed to read event ({}, {})",
+ room_id_.toStdString(),
+ eventId.toStdString());
+ }
+ });
}
QString
@@ -743,7 +930,8 @@ void
TimelineView::removeEvent(const QString &event_id)
{
if (!eventIds_.contains(event_id)) {
- qWarning() << "unknown event_id couldn't be removed:" << event_id;
+ nhlog::ui()->warn("cannot remove widget with unknown event_id: {}",
+ event_id.toStdString());
return;
}
@@ -860,3 +1048,332 @@ TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second)
return diffInSeconds > fifteenMins;
}
+
+void
+TimelineView::sendRoomMessageHandler(const std::string &txn_id,
+ const mtx::responses::EventId &res,
+ mtx::http::RequestErr err)
+{
+ if (err) {
+ const int status_code = static_cast<int>(err->status_code);
+ nhlog::net()->warn("[{}] failed to send message: {} {}",
+ txn_id,
+ err->matrix_error.error,
+ status_code);
+ emit messageFailed(txn_id);
+ return;
+ }
+
+ emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string()));
+}
+
+template<>
+mtx::events::msg::Audio
+toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m)
+{
+ mtx::events::msg::Audio audio;
+ audio.info.mimetype = m.mime.toStdString();
+ audio.info.size = m.media_size;
+ audio.body = m.filename.toStdString();
+ audio.url = m.body.toStdString();
+ return audio;
+}
+
+template<>
+mtx::events::msg::Image
+toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m)
+{
+ mtx::events::msg::Image image;
+ image.info.mimetype = m.mime.toStdString();
+ image.info.size = m.media_size;
+ image.body = m.filename.toStdString();
+ image.url = m.body.toStdString();
+ return image;
+}
+
+template<>
+mtx::events::msg::Video
+toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m)
+{
+ mtx::events::msg::Video video;
+ video.info.mimetype = m.mime.toStdString();
+ video.info.size = m.media_size;
+ video.body = m.filename.toStdString();
+ video.url = m.body.toStdString();
+ return video;
+}
+
+template<>
+mtx::events::msg::Emote
+toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m)
+{
+ mtx::events::msg::Emote emote;
+ emote.body = m.body.toStdString();
+ return emote;
+}
+
+template<>
+mtx::events::msg::File
+toRoomMessage<mtx::events::msg::File>(const PendingMessage &m)
+{
+ mtx::events::msg::File file;
+ file.info.mimetype = m.mime.toStdString();
+ file.info.size = m.media_size;
+ file.body = m.filename.toStdString();
+ file.url = m.body.toStdString();
+ return file;
+}
+
+template<>
+mtx::events::msg::Text
+toRoomMessage<mtx::events::msg::Text>(const PendingMessage &m)
+{
+ mtx::events::msg::Text text;
+ text.body = m.body.toStdString();
+ return text;
+}
+
+void
+TimelineView::prepareEncryptedMessage(const PendingMessage &msg)
+{
+ const auto room_id = room_id_.toStdString();
+
+ using namespace mtx::events;
+ using namespace mtx::identifiers;
+
+ json content;
+
+ // Serialize the message to the plaintext that will be encrypted.
+ switch (msg.ty) {
+ case MessageType::Audio: {
+ content = json(toRoomMessage<msg::Audio>(msg));
+ break;
+ }
+ case MessageType::Emote: {
+ content = json(toRoomMessage<msg::Emote>(msg));
+ break;
+ }
+ case MessageType::File: {
+ content = json(toRoomMessage<msg::File>(msg));
+ break;
+ }
+ case MessageType::Image: {
+ content = json(toRoomMessage<msg::Image>(msg));
+ break;
+ }
+ case MessageType::Text: {
+ content = json(toRoomMessage<msg::Text>(msg));
+ break;
+ }
+ case MessageType::Video: {
+ content = json(toRoomMessage<msg::Video>(msg));
+ break;
+ }
+ default:
+ break;
+ }
+
+ json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}};
+
+ try {
+ // Check if we have already an outbound megolm session then we can use.
+ if (cache::client()->outboundMegolmSessionExists(room_id)) {
+ auto data = olm::encrypt_group_message(
+ room_id, http::v2::client()->device_id(), doc.dump());
+
+ http::v2::client()
+ ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
+ room_id,
+ msg.txn_id,
+ data,
+ std::bind(&TimelineView::sendRoomMessageHandler,
+ this,
+ msg.txn_id,
+ std::placeholders::_1,
+ std::placeholders::_2));
+ return;
+ }
+
+ nhlog::ui()->info("creating new outbound megolm session");
+
+ // Create a new outbound megolm session.
+ auto outbound_session = olm::client()->init_outbound_group_session();
+ const auto session_id = mtx::crypto::session_id(outbound_session.get());
+ const auto session_key = mtx::crypto::session_key(outbound_session.get());
+
+ // TODO: needs to be moved in the lib.
+ auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"},
+ {"room_id", room_id},
+ {"session_id", session_id},
+ {"session_key", session_key}};
+
+ // Saving the new megolm session.
+ // TODO: Maybe it's too early to save.
+ OutboundGroupSessionData session_data;
+ session_data.session_id = session_id;
+ session_data.session_key = session_key;
+ session_data.message_index = 0; // TODO Update me
+ cache::client()->saveOutboundMegolmSession(
+ room_id, session_data, std::move(outbound_session));
+
+ const auto members = cache::client()->roomMembers(room_id);
+ nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
+
+ auto keeper = std::make_shared<StateKeeper>(
+ [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() {
+ try {
+ auto data = olm::encrypt_group_message(
+ room_id, http::v2::client()->device_id(), doc.dump());
+
+ http::v2::client()
+ ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
+ room_id,
+ txn_id,
+ data,
+ std::bind(&TimelineView::sendRoomMessageHandler,
+ this,
+ txn_id,
+ std::placeholders::_1,
+ std::placeholders::_2));
+
+ } catch (const lmdb::error &e) {
+ nhlog::db()->critical(
+ "failed to save megolm outbound session: {}", e.what());
+ }
+ });
+
+ mtx::requests::QueryKeys req;
+ for (const auto &member : members)
+ req.device_keys[member] = {};
+
+ http::v2::client()->query_keys(
+ req,
+ [keeper = std::move(keeper), megolm_payload, this](
+ const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to query device keys: {} {}",
+ err->matrix_error.error,
+ static_cast<int>(err->status_code));
+ // TODO: Mark the event as failed. Communicate with the UI.
+ return;
+ }
+
+ for (const auto &entry : res.device_keys) {
+ for (const auto &dev : entry.second) {
+ nhlog::net()->info("received device {}", dev.first);
+
+ const auto device_keys = dev.second.keys;
+ const auto curveKey = "curve25519:" + dev.first;
+ const auto edKey = "ed25519:" + dev.first;
+
+ if ((device_keys.find(curveKey) == device_keys.end()) ||
+ (device_keys.find(edKey) == device_keys.end())) {
+ nhlog::net()->info(
+ "ignoring malformed keys for device {}",
+ dev.first);
+ continue;
+ }
+
+ DevicePublicKeys pks;
+ pks.ed25519 = device_keys.at(edKey);
+ pks.curve25519 = device_keys.at(curveKey);
+
+ // Validate signatures
+ for (const auto &algo : dev.second.keys) {
+ nhlog::net()->info(
+ "dev keys {} {}", algo.first, algo.second);
+ }
+
+ auto room_key =
+ olm::client()
+ ->create_room_key_event(UserId(dev.second.user_id),
+ pks.ed25519,
+ megolm_payload)
+ .dump();
+
+ http::v2::client()->claim_keys(
+ dev.second.user_id,
+ {dev.second.device_id},
+ std::bind(&TimelineView::handleClaimedKeys,
+ this,
+ keeper,
+ room_key,
+ pks,
+ dev.second.user_id,
+ dev.second.device_id,
+ std::placeholders::_1,
+ std::placeholders::_2));
+ }
+ }
+ });
+
+ } catch (const lmdb::error &e) {
+ nhlog::db()->critical(
+ "failed to open outbound megolm session ({}): {}", room_id, e.what());
+ return;
+ }
+}
+
+void
+TimelineView::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
+ const std::string &room_key,
+ const DevicePublicKeys &pks,
+ const std::string &user_id,
+ const std::string &device_id,
+ const mtx::responses::ClaimKeys &res,
+ mtx::http::RequestErr err)
+{
+ if (err) {
+ nhlog::net()->warn("claim keys error: {}", err->matrix_error.error);
+ return;
+ }
+
+ nhlog::net()->info("claimed keys for {} - {}", user_id, device_id);
+
+ if (res.one_time_keys.size() == 0) {
+ nhlog::net()->info("no one-time keys found for device_id: {}", device_id);
+ return;
+ }
+
+ if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) {
+ nhlog::net()->info(
+ "no one-time keys found in device_id {} for the user {}", device_id, user_id);
+ return;
+ }
+
+ auto retrieved_devices = res.one_time_keys.at(user_id);
+
+ for (const auto &rd : retrieved_devices) {
+ nhlog::net()->info("{} : \n {}", rd.first, rd.second.dump(2));
+
+ // TODO: Verify signatures
+ auto otk = rd.second.begin()->at("key");
+ auto id_key = pks.curve25519;
+
+ auto s = olm::client()->create_outbound_session(id_key, otk);
+
+ auto device_msg =
+ olm::client()->create_olm_encrypted_content(s.get(), room_key, pks.curve25519);
+
+ try {
+ cache::client()->saveOlmSession(id_key, std::move(s));
+ } catch (const lmdb::error &e) {
+ nhlog::db()->critical("failed to save outbound olm session: {}", e.what());
+ } catch (const mtx::crypto::olm_exception &e) {
+ nhlog::crypto()->critical("failed to pickle outbound olm session: {}",
+ e.what());
+ }
+
+ json body{{"messages", {{user_id, {{device_id, device_msg}}}}}};
+
+ http::v2::client()->send_to_device(
+ "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to send "
+ "send_to_device "
+ "message: {}",
+ err->matrix_error.error);
+ }
+ });
+ }
+}
diff --git a/src/timeline/TimelineViewManager.cc b/src/timeline/TimelineViewManager.cc
index b7ce53ae..7ea1ee4a 100644
--- a/src/timeline/TimelineViewManager.cc
+++ b/src/timeline/TimelineViewManager.cc
@@ -18,12 +18,10 @@
#include <random>
#include <QApplication>
-#include <QDebug>
#include <QFileInfo>
#include <QSettings>
-#include "MatrixClient.h"
-
+#include "Logging.hpp"
#include "timeline/TimelineView.h"
#include "timeline/TimelineViewManager.h"
#include "timeline/widgets/AudioItem.h"
@@ -35,42 +33,15 @@ TimelineViewManager::TimelineViewManager(QWidget *parent)
: QStackedWidget(parent)
{
setStyleSheet("border: none;");
-
- connect(
- http::client(), &MatrixClient::messageSent, this, &TimelineViewManager::messageSent);
-
- connect(http::client(),
- &MatrixClient::messageSendFailed,
- this,
- &TimelineViewManager::messageSendFailed);
-
- connect(http::client(),
- &MatrixClient::redactionCompleted,
- this,
- [this](const QString &room_id, const QString &event_id) {
- auto view = views_[room_id];
-
- if (view)
- view->removeEvent(event_id);
- });
}
void
-TimelineViewManager::messageSent(const QString &event_id, const QString &roomid, int txn_id)
+TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id)
{
- // We save the latest valid transaction ID for later use.
- QSettings settings;
- settings.setValue("client/transaction_id", txn_id + 1);
+ auto view = views_[room_id];
- auto view = views_[roomid];
- view->updatePendingMessage(txn_id, event_id);
-}
-
-void
-TimelineViewManager::messageSendFailed(const QString &roomid, int txn_id)
-{
- auto view = views_[roomid];
- view->handleFailedMessage(txn_id);
+ if (view)
+ view->removeEvent(event_id);
}
void
@@ -105,7 +76,7 @@ TimelineViewManager::queueImageMessage(const QString &roomid,
uint64_t size)
{
if (!timelineViewExists(roomid)) {
- qDebug() << "Cannot send m.image message to a non-managed view";
+ nhlog::ui()->warn("Cannot send m.image message to a non-managed view");
return;
}
@@ -122,7 +93,7 @@ TimelineViewManager::queueFileMessage(const QString &roomid,
uint64_t size)
{
if (!timelineViewExists(roomid)) {
- qDebug() << "Cannot send m.file message to a non-managed view";
+ nhlog::ui()->warn("cannot send m.file message to a non-managed view");
return;
}
@@ -139,7 +110,7 @@ TimelineViewManager::queueAudioMessage(const QString &roomid,
uint64_t size)
{
if (!timelineViewExists(roomid)) {
- qDebug() << "Cannot send m.audio message to a non-managed view";
+ nhlog::ui()->warn("cannot send m.audio message to a non-managed view");
return;
}
@@ -156,7 +127,7 @@ TimelineViewManager::queueVideoMessage(const QString &roomid,
uint64_t size)
{
if (!timelineViewExists(roomid)) {
- qDebug() << "Cannot send m.video message to a non-managed view";
+ nhlog::ui()->warn("cannot send m.video message to a non-managed view");
return;
}
@@ -227,7 +198,8 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
auto roomid = QString::fromStdString(room.first);
if (!timelineViewExists(roomid)) {
- qDebug() << "Ignoring event from unknown room" << roomid;
+ nhlog::ui()->warn("ignoring event from unknown room: {}",
+ roomid.toStdString());
continue;
}
@@ -241,7 +213,8 @@ void
TimelineViewManager::setHistoryView(const QString &room_id)
{
if (!timelineViewExists(room_id)) {
- qDebug() << "Room ID from RoomList is not present in ViewManager" << room_id;
+ nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}",
+ room_id.toStdString());
return;
}
diff --git a/src/timeline/widgets/AudioItem.cc b/src/timeline/widgets/AudioItem.cc
index 65ca401b..7cbbed28 100644
--- a/src/timeline/widgets/AudioItem.cc
+++ b/src/timeline/widgets/AudioItem.cc
@@ -16,13 +16,13 @@
*/
#include <QBrush>
-#include <QDebug>
#include <QDesktopServices>
#include <QFile>
#include <QFileDialog>
#include <QPainter>
#include <QPixmap>
+#include "Logging.hpp"
#include "MatrixClient.h"
#include "Utils.h"
@@ -50,21 +50,12 @@ AudioItem::init()
playIcon_.addFile(":/icons/icons/ui/play-sign.png");
pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png");
- QList<QString> url_parts = url_.toString().split("mxc://");
- if (url_parts.size() != 2) {
- qDebug() << "Invalid format for image" << url_.toString();
- return;
- }
-
- QString media_params = url_parts[1];
- url_ = QString("%1/_matrix/media/r0/download/%2")
- .arg(http::client()->getHomeServer().toString(), media_params);
-
player_ = new QMediaPlayer;
player_->setMedia(QUrl(url_));
player_->setVolume(100);
player_->setNotifyInterval(1000);
+ connect(this, &AudioItem::fileDownloadedCb, this, &AudioItem::fileDownloaded);
connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) {
if (state == QMediaPlayer::StoppedState) {
state_ = AudioState::Play;
@@ -129,14 +120,20 @@ AudioItem::mousePressEvent(QMouseEvent *event)
if (filenameToSave_.isEmpty())
return;
- auto proxy = http::client()->downloadFile(url_);
- connect(proxy.data(),
- &DownloadMediaProxy::fileDownloaded,
- this,
- [proxy, this](const QByteArray &data) {
- proxy->deleteLater();
- fileDownloaded(data);
- });
+ http::v2::client()->download(
+ url_.toString().toStdString(),
+ [this](const std::string &data,
+ const std::string &,
+ const std::string &,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->info("failed to retrieve m.audio content: {}",
+ url_.toString().toStdString());
+ return;
+ }
+
+ emit fileDownloadedCb(QByteArray(data.data(), data.size()));
+ });
}
}
@@ -151,8 +148,8 @@ AudioItem::fileDownloaded(const QByteArray &data)
file.write(data);
file.close();
- } catch (const std::exception &ex) {
- qDebug() << "Error while saving file to:" << ex.what();
+ } catch (const std::exception &e) {
+ nhlog::ui()->warn("error while saving file: {}", e.what());
}
}
diff --git a/src/timeline/widgets/FileItem.cc b/src/timeline/widgets/FileItem.cc
index f3906a04..4ce4d256 100644
--- a/src/timeline/widgets/FileItem.cc
+++ b/src/timeline/widgets/FileItem.cc
@@ -16,13 +16,13 @@
*/
#include <QBrush>
-#include <QDebug>
#include <QDesktopServices>
#include <QFile>
#include <QFileDialog>
#include <QPainter>
#include <QPixmap>
+#include "Logging.hpp"
#include "MatrixClient.h"
#include "Utils.h"
@@ -49,17 +49,9 @@ FileItem::init()
icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png");
- QList<QString> url_parts = url_.toString().split("mxc://");
- if (url_parts.size() != 2) {
- qDebug() << "Invalid format for image" << url_.toString();
- return;
- }
-
- QString media_params = url_parts[1];
- url_ = QString("%1/_matrix/media/r0/download/%2")
- .arg(http::client()->getHomeServer().toString(), media_params);
-
setFixedHeight(Height);
+
+ connect(this, &FileItem::fileDownloadedCb, this, &FileItem::fileDownloaded);
}
FileItem::FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, QWidget *parent)
@@ -89,8 +81,15 @@ FileItem::openUrl()
if (url_.toString().isEmpty())
return;
- if (!QDesktopServices::openUrl(url_))
- qWarning() << "Could not open url" << url_.toString();
+ auto mxc_parts = mtx::client::utils::parse_mxc_url(url_.toString().toStdString());
+ auto urlToOpen = QString("https://%1:%2/_matrix/media/r0/download/%3/%4")
+ .arg(QString::fromStdString(http::v2::client()->server()))
+ .arg(http::v2::client()->port())
+ .arg(QString::fromStdString(mxc_parts.server))
+ .arg(QString::fromStdString(mxc_parts.media_id));
+
+ if (!QDesktopServices::openUrl(urlToOpen))
+ nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString());
}
QSize
@@ -115,14 +114,20 @@ FileItem::mousePressEvent(QMouseEvent *event)
if (filenameToSave_.isEmpty())
return;
- auto proxy = http::client()->downloadFile(url_);
- connect(proxy.data(),
- &DownloadMediaProxy::fileDownloaded,
- this,
- [proxy, this](const QByteArray &data) {
- proxy->deleteLater();
- fileDownloaded(data);
- });
+ http::v2::client()->download(
+ url_.toString().toStdString(),
+ [this](const std::string &data,
+ const std::string &,
+ const std::string &,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::ui()->warn("failed to retrieve m.file content: {}",
+ url_.toString().toStdString());
+ return;
+ }
+
+ emit fileDownloadedCb(QByteArray(data.data(), data.size()));
+ });
} else {
openUrl();
}
@@ -139,8 +144,8 @@ FileItem::fileDownloaded(const QByteArray &data)
file.write(data);
file.close();
- } catch (const std::exception &ex) {
- qDebug() << "Error while saving file to:" << ex.what();
+ } catch (const std::exception &e) {
+ nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
}
diff --git a/src/timeline/widgets/ImageItem.cc b/src/timeline/widgets/ImageItem.cc
index 66cd31ab..bf1c05d6 100644
--- a/src/timeline/widgets/ImageItem.cc
+++ b/src/timeline/widgets/ImageItem.cc
@@ -16,7 +16,6 @@
*/
#include <QBrush>
-#include <QDebug>
#include <QDesktopServices>
#include <QFileDialog>
#include <QFileInfo>
@@ -25,42 +24,71 @@
#include <QUuid>
#include "Config.h"
+#include "Logging.hpp"
#include "MatrixClient.h"
#include "Utils.h"
#include "dialogs/ImageOverlay.h"
#include "timeline/widgets/ImageItem.h"
-ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent)
- : QWidget(parent)
- , event_{event}
+void
+ImageItem::downloadMedia(const QUrl &url)
{
- setMouseTracking(true);
- setCursor(Qt::PointingHandCursor);
- setAttribute(Qt::WA_Hover, true);
+ http::v2::client()->download(url.toString().toStdString(),
+ [this, url](const std::string &data,
+ const std::string &,
+ const std::string &,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn(
+ "failed to retrieve image {}: {} {}",
+ url.toString().toStdString(),
+ err->matrix_error.error,
+ static_cast<int>(err->status_code));
+ return;
+ }
- url_ = QString::fromStdString(event.content.url);
- text_ = QString::fromStdString(event.content.body);
+ QPixmap img;
+ img.loadFromData(QByteArray(data.data(), data.size()));
+ emit imageDownloaded(img);
+ });
+}
- QList<QString> url_parts = url_.toString().split("mxc://");
+void
+ImageItem::saveImage(const QString &filename, const QByteArray &data)
+{
+ try {
+ QFile file(filename);
- if (url_parts.size() != 2) {
- qDebug() << "Invalid format for image" << url_.toString();
- return;
+ if (!file.open(QIODevice::WriteOnly))
+ return;
+
+ file.write(data);
+ file.close();
+ } catch (const std::exception &e) {
+ nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
+}
- QString media_params = url_parts[1];
- url_ = QString("%1/_matrix/media/r0/download/%2")
- .arg(http::client()->getHomeServer().toString(), media_params);
+void
+ImageItem::init()
+{
+ setMouseTracking(true);
+ setCursor(Qt::PointingHandCursor);
+ setAttribute(Qt::WA_Hover, true);
- auto proxy = http::client()->downloadImage(url_);
+ connect(this, &ImageItem::imageDownloaded, this, &ImageItem::setImage);
+ connect(this, &ImageItem::imageSaved, this, &ImageItem::saveImage);
+ downloadMedia(url_);
+}
- connect(proxy.data(),
- &DownloadMediaProxy::imageDownloaded,
- this,
- [this, proxy](const QPixmap &img) {
- proxy->deleteLater();
- setImage(img);
- });
+ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent)
+ : QWidget(parent)
+ , event_{event}
+{
+ url_ = QString::fromStdString(event.content.url);
+ text_ = QString::fromStdString(event.content.body);
+
+ init();
}
ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
@@ -69,31 +97,7 @@ ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size,
, text_{filename}
{
Q_UNUSED(size);
-
- setMouseTracking(true);
- setCursor(Qt::PointingHandCursor);
- setAttribute(Qt::WA_Hover, true);
-
- QList<QString> url_parts = url_.toString().split("mxc://");
-
- if (url_parts.size() != 2) {
- qDebug() << "Invalid format for image" << url_.toString();
- return;
- }
-
- QString media_params = url_parts[1];
- url_ = QString("%1/_matrix/media/r0/download/%2")
- .arg(http::client()->getHomeServer().toString(), media_params);
-
- auto proxy = http::client()->downloadImage(url_);
-
- connect(proxy.data(),
- &DownloadMediaProxy::imageDownloaded,
- this,
- [proxy, this](const QPixmap &img) {
- proxy->deleteLater();
- setImage(img);
- });
+ init();
}
void
@@ -102,8 +106,15 @@ ImageItem::openUrl()
if (url_.toString().isEmpty())
return;
- if (!QDesktopServices::openUrl(url_))
- qWarning() << "Could not open url" << url_.toString();
+ auto mxc_parts = mtx::client::utils::parse_mxc_url(url_.toString().toStdString());
+ auto urlToOpen = QString("https://%1:%2/_matrix/media/r0/download/%3/%4")
+ .arg(QString::fromStdString(http::v2::client()->server()))
+ .arg(http::v2::client()->port())
+ .arg(QString::fromStdString(mxc_parts.server))
+ .arg(QString::fromStdString(mxc_parts.media_id));
+
+ if (!QDesktopServices::openUrl(urlToOpen))
+ nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString());
}
QSize
@@ -231,23 +242,22 @@ ImageItem::saveAs()
if (filename.isEmpty())
return;
- auto proxy = http::client()->downloadFile(url_);
- connect(proxy.data(),
- &DownloadMediaProxy::fileDownloaded,
- this,
- [proxy, filename](const QByteArray &data) {
- proxy->deleteLater();
-
- try {
- QFile file(filename);
+ const auto url = url_.toString().toStdString();
- if (!file.open(QIODevice::WriteOnly))
- return;
+ http::v2::client()->download(
+ url,
+ [this, filename, url](const std::string &data,
+ const std::string &,
+ const std::string &,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to retrieve image {}: {} {}",
+ url,
+ err->matrix_error.error,
+ static_cast<int>(err->status_code));
+ return;
+ }
- file.write(data);
- file.close();
- } catch (const std::exception &ex) {
- qDebug() << "Error while saving file to:" << ex.what();
- }
- });
+ emit imageSaved(filename, QByteArray(data.data(), data.size()));
+ });
}
diff --git a/src/timeline/widgets/VideoItem.cc b/src/timeline/widgets/VideoItem.cc
index f5bcfd6e..34d963a9 100644
--- a/src/timeline/widgets/VideoItem.cc
+++ b/src/timeline/widgets/VideoItem.cc
@@ -15,7 +15,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#include <QDebug>
#include <QLabel>
#include <QVBoxLayout>
@@ -27,15 +26,15 @@
void
VideoItem::init()
{
- QList<QString> url_parts = url_.toString().split("mxc://");
- if (url_parts.size() != 2) {
- qDebug() << "Invalid format for image" << url_.toString();
- return;
- }
+ // QList<QString> url_parts = url_.toString().split("mxc://");
+ // if (url_parts.size() != 2) {
+ // qDebug() << "Invalid format for image" << url_.toString();
+ // return;
+ // }
- QString media_params = url_parts[1];
- url_ = QString("%1/_matrix/media/r0/download/%2")
- .arg(http::client()->getHomeServer().toString(), media_params);
+ // QString media_params = url_parts[1];
+ // url_ = QString("%1/_matrix/media/r0/download/%2")
+ // .arg(http::client()->getHomeServer().toString(), media_params);
}
VideoItem::VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, QWidget *parent)
|