matrixion/src/ui/UserProfile.cpp

627 lines
18 KiB
C++
Raw Normal View History

// SPDX-FileCopyrightText: Nheko Contributors
2021-03-05 02:35:15 +03:00
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QFileDialog>
#include <QImageReader>
#include <QMimeDatabase>
#include <QStandardPaths>
2022-06-16 02:19:26 +03:00
#include "Cache.h"
#include "Cache_p.h"
2020-06-26 01:54:42 +03:00
#include "ChatPage.h"
2020-05-17 16:34:47 +03:00
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "UserProfile.h"
2020-05-17 16:34:47 +03:00
#include "Utils.h"
2022-06-16 02:19:26 +03:00
#include "encryption/VerificationManager.h"
2020-08-09 06:05:15 +03:00
#include "timeline/TimelineModel.h"
#include "timeline/TimelineViewManager.h"
#include "ui/UIA.h"
2020-05-17 16:34:47 +03:00
UserProfile::UserProfile(const QString &roomid,
const QString &userid,
TimelineViewManager *manager_,
TimelineModel *parent)
2020-07-04 05:24:28 +03:00
: QObject(parent)
, roomid_(roomid)
, userid_(userid)
, globalAvatarUrl{QLatin1String("")}
, manager(manager_)
2020-08-09 06:05:15 +03:00
, model(parent)
2020-07-04 05:24:28 +03:00
{
2021-09-18 01:22:33 +03:00
connect(this,
&UserProfile::globalUsernameRetrieved,
this,
&UserProfile::setGlobalUsername,
Qt::QueuedConnection);
connect(this,
&UserProfile::verificationStatiChanged,
this,
&UserProfile::updateVerificationStatus,
Qt::QueuedConnection);
2021-09-18 01:22:33 +03:00
if (isGlobalUserProfile()) {
getGlobalProfileData();
}
if (!cache::client() || !cache::client()->isDatabaseReady() ||
!ChatPage::instance()->timelineManager())
return;
connect(
cache::client(), &Cache::verificationStatusChanged, this, [this](const std::string &user_id) {
if (user_id != this->userid_.toStdString())
return;
emit verificationStatiChanged();
2021-09-18 01:22:33 +03:00
});
fetchDeviceList(this->userid_);
2023-02-24 04:40:14 +03:00
if (userid != utils::localUser())
sharedRooms_ =
new RoomInfoModel(cache::client()->getCommonRooms(userid.toStdString()), this);
else
sharedRooms_ = new RoomInfoModel({}, this);
connect(ChatPage::instance(), &ChatPage::syncUI, this, [this](const mtx::responses::Sync &res) {
if (auto ignoreEv = std::ranges::find_if(
res.account_data.events,
[](const mtx::events::collections::RoomAccountDataEvents &e) {
return std::holds_alternative<
mtx::events::AccountDataEvent<mtx::events::account_data::IgnoredUsers>>(e);
});
ignoreEv != res.account_data.events.end()) {
// doesn't matter much if it was actually us
emit ignoredChanged();
}
});
2020-07-04 05:24:28 +03:00
}
2020-06-28 18:31:34 +03:00
2020-07-04 05:24:28 +03:00
QHash<int, QByteArray>
DeviceInfoModel::roleNames() const
{
2021-09-18 01:22:33 +03:00
return {
{DeviceId, "deviceId"},
{DeviceName, "deviceName"},
{VerificationStatus, "verificationStatus"},
{LastIp, "lastIp"},
{LastTs, "lastTs"},
2021-09-18 01:22:33 +03:00
};
2020-07-04 05:24:28 +03:00
}
2020-07-01 15:17:10 +03:00
2020-07-04 05:24:28 +03:00
QVariant
DeviceInfoModel::data(const QModelIndex &index, int role) const
2020-07-01 15:17:10 +03:00
{
2021-09-18 01:22:33 +03:00
if (!index.isValid() || index.row() >= (int)deviceList_.size() || index.row() < 0)
return {};
switch (role) {
case DeviceId:
return deviceList_[index.row()].device_id;
case DeviceName:
return deviceList_[index.row()].display_name;
case VerificationStatus:
return QVariant::fromValue(deviceList_[index.row()].verification_status);
case LastIp:
return deviceList_[index.row()].lastIp;
case LastTs:
return deviceList_[index.row()].lastTs;
2021-09-18 01:22:33 +03:00
default:
return {};
}
2020-07-01 15:17:10 +03:00
}
2020-05-17 16:34:47 +03:00
2020-07-04 05:24:28 +03:00
void
DeviceInfoModel::reset(const std::vector<DeviceInfo> &deviceList)
2020-05-27 11:49:26 +03:00
{
2021-09-18 01:22:33 +03:00
beginResetModel();
this->deviceList_ = std::move(deviceList);
endResetModel();
2020-07-04 05:24:28 +03:00
}
2023-02-24 04:40:14 +03:00
RoomInfoModel::RoomInfoModel(const std::map<std::string, RoomInfo> &raw, QObject *parent)
: QAbstractListModel(parent)
{
for (const auto &e : raw)
roomInfos_.push_back(e);
}
QHash<int, QByteArray>
RoomInfoModel::roleNames() const
{
return {
{RoomId, "roomId"},
{RoomName, "roomName"},
{AvatarUrl, "avatarUrl"},
};
}
QVariant
RoomInfoModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= (int)roomInfos_.size() || index.row() < 0)
return {};
switch (role) {
case RoomId:
return QString::fromStdString(roomInfos_[index.row()].first);
case RoomName:
return QString::fromStdString(roomInfos_[index.row()].second.name);
case AvatarUrl:
return QString::fromStdString(roomInfos_[index.row()].second.avatar_url);
default:
return {};
}
}
2020-07-04 05:24:28 +03:00
DeviceInfoModel *
UserProfile::deviceList()
{
2021-09-18 01:22:33 +03:00
return &this->deviceList_;
2020-05-17 16:34:47 +03:00
}
2023-02-24 04:40:14 +03:00
RoomInfoModel *
UserProfile::sharedRooms()
{
return this->sharedRooms_;
}
2020-05-22 08:47:02 +03:00
QString
2020-07-04 05:24:28 +03:00
UserProfile::userid()
2020-05-27 11:49:26 +03:00
{
2021-09-18 01:22:33 +03:00
return this->userid_;
2020-05-22 08:47:02 +03:00
}
2020-07-04 05:24:28 +03:00
QString
UserProfile::displayName()
2020-05-27 11:49:26 +03:00
{
2021-09-18 01:22:33 +03:00
return isGlobalUserProfile() ? globalUsername : cache::displayName(roomid_, userid_);
2020-07-04 05:24:28 +03:00
}
QString
UserProfile::avatarUrl()
{
2021-09-18 01:22:33 +03:00
return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_);
2020-05-22 08:47:02 +03:00
}
bool
2021-01-29 09:25:24 +03:00
UserProfile::isGlobalUserProfile() const
{
return roomid_ == QLatin1String("");
}
crypto::Trust
2020-07-17 23:16:30 +03:00
UserProfile::getUserStatus()
{
2021-09-18 01:22:33 +03:00
return isUserVerified;
2020-07-17 23:16:30 +03:00
}
bool
UserProfile::userVerificationEnabled() const
{
2021-09-18 01:22:33 +03:00
return hasMasterKey;
}
bool
UserProfile::isSelf() const
{
2021-09-18 01:22:33 +03:00
return this->userid_ == utils::localUser();
}
void
UserProfile::signOutDevice(const QString &deviceID)
{
http::client()->delete_device(
deviceID.toStdString(),
UIA::instance()->genericHandler(tr("Sign out device %1").arg(deviceID)),
[this, deviceID](mtx::http::RequestErr e) {
if (e) {
nhlog::ui()->critical("Failure when attempting to sign out device {}",
deviceID.toStdString());
return;
}
nhlog::ui()->info("Device {} successfully signed out!", deviceID.toStdString());
// This is us. Let's update the interface accordingly
if (isSelf() && deviceID.toStdString() == ::http::client()->device_id()) {
ChatPage::instance()->dropToLoginPageCb(tr("You signed out this device."));
}
refreshDevices();
});
}
void
UserProfile::refreshDevices()
{
2021-10-07 20:59:03 +03:00
cache::client()->markUserKeysOutOfDate({this->userid_.toStdString()});
fetchDeviceList(this->userid_);
}
bool
UserProfile::ignored() const
{
auto old = TimelineViewManager::instance()->getIgnoredUsers();
return old.contains(userid_);
}
2023-07-28 04:04:34 +03:00
void
UserProfile::setIgnored(bool ignore)
2023-07-28 04:04:34 +03:00
{
2023-09-08 00:10:04 +03:00
auto old = TimelineViewManager::instance()->getIgnoredUsers();
2023-07-28 04:04:34 +03:00
if (ignore) {
if (old.contains(userid_)) {
emit ignoredChanged();
2023-07-28 04:04:34 +03:00
return;
}
old.append(userid_);
2023-07-28 04:04:34 +03:00
} else {
if (!old.contains(userid_)) {
emit ignoredChanged();
return;
}
old.removeAll(userid_);
2023-07-28 04:04:34 +03:00
}
std::vector<mtx::events::account_data::IgnoredUser> content;
for (const QString &item : std::as_const(old)) {
content.push_back({item.toStdString()});
2023-07-28 04:04:34 +03:00
}
mtx::events::account_data::IgnoredUsers payload{.users{content}};
auto userid = userid_;
2023-07-28 04:04:34 +03:00
http::client()->put_account_data(payload, [userid](mtx::http::RequestErr e) {
if (e) {
MainWindow::instance()->showNotification(
tr("Failed to ignore \"%1\": %2")
.arg(userid, QString::fromStdString(e->matrix_error.error)));
2023-07-28 04:04:34 +03:00
}
});
if (ignore) {
const QHash<QString, RoomInfo> invites = cache::invites();
for (auto room = invites.keyBegin(), end = invites.keyEnd(); room != end; room++) {
FilteredRoomlistModel::instance()->declineInvite(*room);
}
}
2023-07-28 04:04:34 +03:00
}
2020-06-28 18:31:34 +03:00
void
UserProfile::fetchDeviceList(const QString &userID)
{
2021-09-18 01:22:33 +03:00
if (!cache::client() || !cache::client()->isDatabaseReady())
return;
cache::client()->query_keys(
userID.toStdString(),
[other_user_id = userID.toStdString(), this](const UserKeyCache &,
2021-09-18 01:22:33 +03:00
mtx::http::RequestErr err) {
if (err) {
2023-01-09 04:06:49 +03:00
nhlog::net()->warn("failed to query device keys: {}", *err);
2021-09-18 01:22:33 +03:00
}
// Ensure local key cache is up to date
cache::client()->query_keys(
utils::localUser().toStdString(),
[this](const UserKeyCache &, mtx::http::RequestErr err) {
2021-09-18 01:22:33 +03:00
using namespace mtx;
std::string local_user_id = utils::localUser().toStdString();
if (err) {
2023-01-09 04:06:49 +03:00
nhlog::net()->warn("failed to query device keys: {}", *err);
2021-09-18 01:22:33 +03:00
}
emit verificationStatiChanged();
});
});
}
2021-09-18 01:22:33 +03:00
void
UserProfile::updateVerificationStatus()
{
if (!cache::client() || !cache::client()->isDatabaseReady())
return;
2021-09-18 01:22:33 +03:00
auto user_keys = cache::client()->userKeys(userid_.toStdString());
if (!user_keys) {
this->hasMasterKey = false;
this->isUserVerified = crypto::Trust::Unverified;
this->deviceList_.reset({});
emit userStatusChanged();
return;
}
2021-09-18 01:22:33 +03:00
this->hasMasterKey = !user_keys->master_keys.keys.empty();
2021-09-18 01:22:33 +03:00
std::vector<DeviceInfo> deviceInfo;
auto devices = user_keys->device_keys;
auto verificationStatus = cache::client()->verificationStatus(userid_.toStdString());
2021-09-18 01:22:33 +03:00
this->isUserVerified = verificationStatus.user_verified;
emit userStatusChanged();
deviceInfo.reserve(devices.size());
for (const auto &d : devices) {
auto device = d.second;
verification::Status verified =
std::find(verificationStatus.verified_devices.begin(),
verificationStatus.verified_devices.end(),
device.device_id) == verificationStatus.verified_devices.end()
? verification::UNVERIFIED
: verification::VERIFIED;
2021-09-18 01:22:33 +03:00
if (isSelf() && device.device_id == ::http::client()->device_id())
verified = verification::Status::SELF;
deviceInfo.emplace_back(QString::fromStdString(d.first),
QString::fromStdString(device.unsigned_info.device_display_name),
verified);
}
// For self, also query devices without keys
if (isSelf()) {
http::client()->query_devices(
[this, deviceInfo](const mtx::responses::QueryDevices &allDevs,
mtx::http::RequestErr err) mutable {
if (err) {
2023-01-09 04:06:49 +03:00
nhlog::net()->warn("failed to query device keys: {}", *err);
this->deviceList_.queueReset(std::move(deviceInfo));
emit devicesChanged();
return;
}
for (const auto &d : allDevs.devices) {
// First, check if we already have an entry for this device
bool found = false;
for (auto &e : deviceInfo) {
if (e.device_id.toStdString() == d.device_id) {
found = true;
// Gottem! Let's fill in the blanks
e.lastIp = QString::fromStdString(d.last_seen_ip);
e.lastTs = static_cast<qlonglong>(d.last_seen_ts);
break;
}
}
// No entry? Let's add one.
if (!found) {
deviceInfo.emplace_back(QString::fromStdString(d.device_id),
QString::fromStdString(d.display_name),
verification::NOT_APPLICABLE,
QString::fromStdString(d.last_seen_ip),
d.last_seen_ts);
}
}
this->deviceList_.queueReset(std::move(deviceInfo));
emit devicesChanged();
});
return;
}
this->deviceList_.queueReset(std::move(deviceInfo));
emit devicesChanged();
2020-05-17 16:34:47 +03:00
}
2020-05-27 11:49:26 +03:00
2020-06-26 01:54:42 +03:00
void
UserProfile::banUser()
{
2022-05-07 19:53:16 +03:00
ChatPage::instance()->banUser(roomid_, this->userid_, QLatin1String(""));
2020-06-26 01:54:42 +03:00
}
void
UserProfile::kickUser()
{
2022-05-07 19:53:16 +03:00
ChatPage::instance()->kickUser(roomid_, this->userid_, QLatin1String(""));
2020-06-26 01:54:42 +03:00
}
void
UserProfile::startChat(bool encryption)
{
ChatPage::instance()->startChat(this->userid_, encryption);
}
2020-06-26 01:54:42 +03:00
void
UserProfile::startChat()
{
ChatPage::instance()->startChat(this->userid_, std::nullopt);
2020-07-17 23:16:30 +03:00
}
void
UserProfile::changeUsername(const QString &username)
{
2021-09-18 01:22:33 +03:00
if (isGlobalUserProfile()) {
// change global
http::client()->set_displayname(username.toStdString(), [](mtx::http::RequestErr err) {
if (err) {
2023-01-09 04:06:49 +03:00
nhlog::net()->warn("could not change username: {}", *err);
2021-09-18 01:22:33 +03:00
return;
}
});
} else {
// change room username
mtx::events::state::Member member;
member.display_name = username.toStdString();
member.avatar_url =
cache::avatarUrl(roomid_, QString::fromStdString(http::client()->user_id().to_string()))
.toStdString();
member.membership = mtx::events::state::Membership::Join;
updateRoomMemberState(std::move(member));
}
}
void
UserProfile::changeDeviceName(const QString &deviceID, const QString &deviceName)
{
http::client()->set_device_name(
deviceID.toStdString(), deviceName.toStdString(), [this](mtx::http::RequestErr err) {
if (err) {
2023-01-09 04:06:49 +03:00
nhlog::net()->warn("could not change device name: {}", *err);
return;
}
refreshDevices();
});
}
void
UserProfile::verify(QString device)
2020-07-17 23:16:30 +03:00
{
2021-09-18 01:22:33 +03:00
if (!device.isEmpty())
manager->verificationManager()->verifyDevice(userid_, device);
2021-09-18 01:22:33 +03:00
else {
manager->verificationManager()->verifyUser(userid_);
2021-09-18 01:22:33 +03:00
}
2020-08-30 20:33:10 +03:00
}
void
UserProfile::unverify(const QString &device)
{
2021-09-18 01:22:33 +03:00
cache::markDeviceUnverified(userid_.toStdString(), device.toStdString());
}
void
2021-01-28 21:45:40 +03:00
UserProfile::setGlobalUsername(const QString &globalUser)
{
2021-09-18 01:22:33 +03:00
globalUsername = globalUser;
emit displayNameChanged();
}
void
UserProfile::changeAvatar()
{
2021-09-18 01:22:33 +03:00
const QString picturesFolder =
QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
const QString fileName = QFileDialog::getOpenFileName(
nullptr, tr("Select an avatar"), picturesFolder, tr("All Files (*)"));
if (fileName.isEmpty())
return;
QMimeDatabase db;
QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
const auto format = mime.name().split(QStringLiteral("/"))[0];
2021-09-18 01:22:33 +03:00
QFile file{fileName, this};
if (format != QLatin1String("image")) {
2021-09-18 01:22:33 +03:00
emit displayError(tr("The selected file is not an image"));
return;
}
if (!file.open(QIODevice::ReadOnly)) {
emit displayError(tr("Error while reading file: %1").arg(file.errorString()));
return;
}
const auto bin = file.peek(file.size());
const auto payload = std::string(bin.data(), bin.size());
isLoading_ = true;
emit loadingChanged();
// First we need to create a new mxc URI
// (i.e upload media to the Matrix content repository) for the new avatar.
http::client()->upload(
payload,
mime.name().toStdString(),
QFileInfo(fileName).fileName().toStdString(),
[this,
payload,
mimetype = mime.name().toStdString(),
size = payload.size(),
room_id = roomid_.toStdString(),
content = std::move(bin)](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) {
if (err) {
2023-01-09 04:06:49 +03:00
nhlog::ui()->error("Failed to upload image: {}", *err);
2021-09-18 01:22:33 +03:00
return;
}
if (isGlobalUserProfile()) {
http::client()->set_avatar_url(res.content_uri, [this](mtx::http::RequestErr err) {
if (err) {
2023-01-09 04:06:49 +03:00
nhlog::ui()->error("Failed to set user avatar url: {}", *err);
}
2021-09-18 01:22:33 +03:00
isLoading_ = false;
emit loadingChanged();
getGlobalProfileData();
});
} else {
// change room username
mtx::events::state::Member member;
member.display_name = cache::displayName(roomid_, userid_).toStdString();
member.avatar_url = res.content_uri;
member.membership = mtx::events::state::Membership::Join;
updateRoomMemberState(std::move(member));
}
});
}
void
UserProfile::updateRoomMemberState(mtx::events::state::Member member)
{
2023-01-09 04:06:49 +03:00
http::client()->send_state_event(roomid_.toStdString(),
http::client()->user_id().to_string(),
member,
[](mtx::responses::EventId, mtx::http::RequestErr err) {
if (err)
nhlog::net()->error(
"Failed to update room member state: {}", *err);
});
2021-02-02 14:54:08 +03:00
}
void
UserProfile::updateAvatarUrl()
{
2021-09-18 01:22:33 +03:00
isLoading_ = false;
emit loadingChanged();
2021-02-02 14:54:08 +03:00
2021-09-18 01:22:33 +03:00
emit avatarUrlChanged();
2021-02-02 14:54:08 +03:00
}
bool
UserProfile::isLoading() const
{
2021-09-18 01:22:33 +03:00
return isLoading_;
}
void
UserProfile::getGlobalProfileData()
{
2022-08-13 17:28:41 +03:00
auto profProx = std::make_shared<UserProfileFetchProxy>();
connect(profProx.get(),
&UserProfileFetchProxy::profileFetched,
this,
[this](const mtx::responses::Profile &res) {
emit globalUsernameRetrieved(QString::fromStdString(res.display_name));
globalAvatarUrl = QString::fromStdString(res.avatar_url);
emit avatarUrlChanged();
});
2021-09-18 01:22:33 +03:00
2023-12-12 04:29:36 +03:00
connect(profProx.get(),
&UserProfileFetchProxy::failedToFetchProfile,
this,
&UserProfile::failedToFetchProfile);
2022-08-13 17:28:41 +03:00
http::client()->get_profile(userid_.toStdString(),
[prox = std::move(profProx), user = userid_.toStdString()](
const mtx::responses::Profile &res, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve profile info for {}",
user);
2023-12-12 04:29:36 +03:00
emit prox->failedToFetchProfile();
2022-08-13 17:28:41 +03:00
return;
}
emit prox->profileFetched(res);
});
}
void
UserProfile::openGlobalProfile()
{
2021-09-18 01:22:33 +03:00
emit manager->openGlobalUserProfile(userid_);
}