matrixion/src/ui/UserProfile.cpp
2023-10-01 22:52:40 -03:00

588 lines
17 KiB
C++

// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QFileDialog>
#include <QImageReader>
#include <QMimeDatabase>
#include <QStandardPaths>
#include "Cache.h"
#include "Cache_p.h"
#include "ChatPage.h"
#include "Logging.h"
#include "UserProfile.h"
#include "Utils.h"
#include "encryption/DeviceVerificationFlow.h"
#include "encryption/VerificationManager.h"
#include "mtx/responses/crypto.hpp"
#include "timeline/TimelineModel.h"
#include "timeline/TimelineViewManager.h"
#include "ui/UIA.h"
UserProfile::UserProfile(const QString &roomid,
const QString &userid,
TimelineViewManager *manager_,
TimelineModel *parent)
: QObject(parent)
, roomid_(roomid)
, userid_(userid)
, globalAvatarUrl{QLatin1String("")}
, manager(manager_)
, model(parent)
{
connect(this,
&UserProfile::globalUsernameRetrieved,
this,
&UserProfile::setGlobalUsername,
Qt::QueuedConnection);
connect(this,
&UserProfile::verificationStatiChanged,
this,
&UserProfile::updateVerificationStatus,
Qt::QueuedConnection);
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();
});
fetchDeviceList(this->userid_);
if (userid != utils::localUser())
sharedRooms_ =
new RoomInfoModel(cache::client()->getCommonRooms(userid.toStdString()), this);
else
sharedRooms_ = new RoomInfoModel({}, this);
}
QHash<int, QByteArray>
DeviceInfoModel::roleNames() const
{
return {
{DeviceId, "deviceId"},
{DeviceName, "deviceName"},
{VerificationStatus, "verificationStatus"},
{LastIp, "lastIp"},
{LastTs, "lastTs"},
};
}
QVariant
DeviceInfoModel::data(const QModelIndex &index, int role) const
{
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;
default:
return {};
}
}
void
DeviceInfoModel::reset(const std::vector<DeviceInfo> &deviceList)
{
beginResetModel();
this->deviceList_ = std::move(deviceList);
endResetModel();
}
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 {};
}
}
DeviceInfoModel *
UserProfile::deviceList()
{
return &this->deviceList_;
}
RoomInfoModel *
UserProfile::sharedRooms()
{
return this->sharedRooms_;
}
QString
UserProfile::userid()
{
return this->userid_;
}
QString
UserProfile::displayName()
{
return isGlobalUserProfile() ? globalUsername : cache::displayName(roomid_, userid_);
}
QString
UserProfile::avatarUrl()
{
return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_);
}
bool
UserProfile::isGlobalUserProfile() const
{
return roomid_ == QLatin1String("");
}
crypto::Trust
UserProfile::getUserStatus()
{
return isUserVerified;
}
bool
UserProfile::userVerificationEnabled() const
{
return hasMasterKey;
}
bool
UserProfile::isSelf() const
{
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()
{
cache::client()->markUserKeysOutOfDate({this->userid_.toStdString()});
fetchDeviceList(this->userid_);
}
void
UserProfile::ignoredStatus(const QString &id, const bool ignore)
{
auto old = TimelineViewManager::instance()->getIgnoredUsers();
if (ignore) {
if (old.contains(id)) {
emit this->room()->ignoredUser(id, tr("Already ignored"));
return;
}
old.append(id);
} else {
old.removeOne(id);
}
std::vector<mtx::events::account_data::IgnoredUser> content;
for (const QString &item : old) {
const mtx::events::account_data::IgnoredUser data{.id = item.toStdString()};
content.push_back(data);
}
const mtx::events::account_data::IgnoredUsers payload{.users{content}};
http::client()->put_account_data(payload, [this, id, ignore](mtx::http::RequestErr e) {
if (ignore) {
emit this->room()->ignoredUser(
id, e ? std::optional(QString::fromStdString(e->matrix_error.error)) : std::nullopt);
} else if (e) {
emit this->unignoredUserError(id, QString::fromStdString(e->matrix_error.error));
}
});
}
void
UserProfile::fetchDeviceList(const QString &userID)
{
if (!cache::client() || !cache::client()->isDatabaseReady())
return;
cache::client()->query_keys(
userID.toStdString(),
[other_user_id = userID.toStdString(), this](const UserKeyCache &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to query device keys: {}", *err);
}
// Ensure local key cache is up to date
cache::client()->query_keys(
utils::localUser().toStdString(),
[this](const UserKeyCache &, mtx::http::RequestErr err) {
using namespace mtx;
std::string local_user_id = utils::localUser().toStdString();
if (err) {
nhlog::net()->warn("failed to query device keys: {}", *err);
}
emit verificationStatiChanged();
});
});
}
void
UserProfile::updateVerificationStatus()
{
if (!cache::client() || !cache::client()->isDatabaseReady())
return;
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;
}
this->hasMasterKey = !user_keys->master_keys.keys.empty();
std::vector<DeviceInfo> deviceInfo;
auto devices = user_keys->device_keys;
auto verificationStatus = cache::client()->verificationStatus(userid_.toStdString());
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;
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) {
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();
}
void
UserProfile::banUser()
{
ChatPage::instance()->banUser(roomid_, this->userid_, QLatin1String(""));
}
void
UserProfile::kickUser()
{
ChatPage::instance()->kickUser(roomid_, this->userid_, QLatin1String(""));
}
void
UserProfile::startChat(bool encryption)
{
ChatPage::instance()->startChat(this->userid_, encryption);
}
void
UserProfile::startChat()
{
ChatPage::instance()->startChat(this->userid_, std::nullopt);
}
void
UserProfile::changeUsername(const QString &username)
{
if (isGlobalUserProfile()) {
// change global
http::client()->set_displayname(username.toStdString(), [](mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("could not change username: {}", *err);
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) {
nhlog::net()->warn("could not change device name: {}", *err);
return;
}
refreshDevices();
});
}
void
UserProfile::verify(QString device)
{
if (!device.isEmpty())
manager->verificationManager()->verifyDevice(userid_, device);
else {
manager->verificationManager()->verifyUser(userid_);
}
}
void
UserProfile::unverify(const QString &device)
{
cache::markDeviceUnverified(userid_.toStdString(), device.toStdString());
}
void
UserProfile::setGlobalUsername(const QString &globalUser)
{
globalUsername = globalUser;
emit displayNameChanged();
}
void
UserProfile::changeAvatar()
{
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];
QFile file{fileName, this};
if (format != QLatin1String("image")) {
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) {
nhlog::ui()->error("Failed to upload image: {}", *err);
return;
}
if (isGlobalUserProfile()) {
http::client()->set_avatar_url(res.content_uri, [this](mtx::http::RequestErr err) {
if (err) {
nhlog::ui()->error("Failed to set user avatar url: {}", *err);
}
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)
{
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);
});
}
void
UserProfile::updateAvatarUrl()
{
isLoading_ = false;
emit loadingChanged();
emit avatarUrlChanged();
}
bool
UserProfile::isLoading() const
{
return isLoading_;
}
void
UserProfile::getGlobalProfileData()
{
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();
});
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);
return;
}
emit prox->profileFetched(res);
});
}
void
UserProfile::openGlobalProfile()
{
emit manager->openGlobalUserProfile(userid_);
}