Newer
Older
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QFileDialog>
#include <QImageReader>
#include <QMimeDatabase>
#include <QStandardPaths>
Nicolas Werner
committed
#include "Cache_p.h"
#include "encryption/DeviceVerificationFlow.h"
#include "timeline/TimelineViewManager.h"
#include "ui/UIA.h"
UserProfile::UserProfile(const QString &roomid,
const QString &userid,
TimelineModel *parent)
: QObject(parent)
, roomid_(roomid)
, userid_(userid)
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();
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;
void
DeviceInfoModel::reset(const std::vector<DeviceInfo> &deviceList)
beginResetModel();
this->deviceList_ = std::move(deviceList);
endResetModel();
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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()
{
RoomInfoModel *
UserProfile::sharedRooms()
{
return this->sharedRooms_;
}
return isGlobalUserProfile() ? globalUsername : cache::displayName(roomid_, userid_);
}
QString
UserProfile::avatarUrl()
{
return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_);
bool
{
return roomid_ == QLatin1String("");
}
UserProfile::getUserStatus()
{
Nicolas Werner
committed
bool
UserProfile::userVerificationEnabled() const
{
Nicolas Werner
committed
}
bool
UserProfile::isSelf() const
{
return this->userid_ == utils::localUser();
Nicolas Werner
committed
}
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();
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);
}
ChatPage::instance()->startChat(this->userid_, std::nullopt);
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));
}
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)
manager->verificationManager()->verifyDevice(userid_, device);
manager->verificationManager()->verifyUser(userid_);
cache::markDeviceUnverified(userid_.toStdString(), device.toStdString());
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];
if (format != QLatin1String("image")) {
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
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) {
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);
});
isLoading_ = false;
emit loadingChanged();
}
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_);