Newer
Older
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
// SPDX-FileCopyrightText: 2023 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();
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();
}
DeviceInfoModel *
UserProfile::deviceList()
{
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::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 ignoreUser(){
// }
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")) {
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
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_);