1 // SPDX-FileCopyrightText: 2021 Nheko Contributors
2 //
3 // SPDX-License-Identifier: GPL-3.0-or-later
4
5 #include <QFileDialog>
6 #include <QImageReader>
7 #include <QMimeDatabase>
8 #include <QStandardPaths>
9
10 #include "Cache_p.h"
11 #include "ChatPage.h"
12 #include "Logging.h"
13 #include "UserProfile.h"
14 #include "Utils.h"
15 #include "encryption/DeviceVerificationFlow.h"
16 #include "mtx/responses/crypto.hpp"
17 #include "timeline/TimelineModel.h"
18 #include "timeline/TimelineViewManager.h"
19 #include "ui/UIA.h"
20
UserProfile(QString roomid,QString userid,TimelineViewManager * manager_,TimelineModel * parent)21 UserProfile::UserProfile(QString roomid,
22 QString userid,
23 TimelineViewManager *manager_,
24 TimelineModel *parent)
25 : QObject(parent)
26 , roomid_(roomid)
27 , userid_(userid)
28 , manager(manager_)
29 , model(parent)
30 {
31 globalAvatarUrl = "";
32
33 connect(this,
34 &UserProfile::globalUsernameRetrieved,
35 this,
36 &UserProfile::setGlobalUsername,
37 Qt::QueuedConnection);
38 connect(this, &UserProfile::verificationStatiChanged, &UserProfile::updateVerificationStatus);
39
40 if (isGlobalUserProfile()) {
41 getGlobalProfileData();
42 }
43
44 if (!cache::client() || !cache::client()->isDatabaseReady() ||
45 !ChatPage::instance()->timelineManager())
46 return;
47
48 connect(
49 cache::client(), &Cache::verificationStatusChanged, this, [this](const std::string &user_id) {
50 if (user_id != this->userid_.toStdString())
51 return;
52
53 emit verificationStatiChanged();
54 });
55 fetchDeviceList(this->userid_);
56 }
57
58 QHash<int, QByteArray>
roleNames() const59 DeviceInfoModel::roleNames() const
60 {
61 return {
62 {DeviceId, "deviceId"},
63 {DeviceName, "deviceName"},
64 {VerificationStatus, "verificationStatus"},
65 {LastIp, "lastIp"},
66 {LastTs, "lastTs"},
67 };
68 }
69
70 QVariant
data(const QModelIndex & index,int role) const71 DeviceInfoModel::data(const QModelIndex &index, int role) const
72 {
73 if (!index.isValid() || index.row() >= (int)deviceList_.size() || index.row() < 0)
74 return {};
75
76 switch (role) {
77 case DeviceId:
78 return deviceList_[index.row()].device_id;
79 case DeviceName:
80 return deviceList_[index.row()].display_name;
81 case VerificationStatus:
82 return QVariant::fromValue(deviceList_[index.row()].verification_status);
83 case LastIp:
84 return deviceList_[index.row()].lastIp;
85 case LastTs:
86 return deviceList_[index.row()].lastTs;
87 default:
88 return {};
89 }
90 }
91
92 void
reset(const std::vector<DeviceInfo> & deviceList)93 DeviceInfoModel::reset(const std::vector<DeviceInfo> &deviceList)
94 {
95 beginResetModel();
96 this->deviceList_ = std::move(deviceList);
97 endResetModel();
98 }
99
100 DeviceInfoModel *
deviceList()101 UserProfile::deviceList()
102 {
103 return &this->deviceList_;
104 }
105
106 QString
userid()107 UserProfile::userid()
108 {
109 return this->userid_;
110 }
111
112 QString
displayName()113 UserProfile::displayName()
114 {
115 return isGlobalUserProfile() ? globalUsername : cache::displayName(roomid_, userid_);
116 }
117
118 QString
avatarUrl()119 UserProfile::avatarUrl()
120 {
121 return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_);
122 }
123
124 bool
isGlobalUserProfile() const125 UserProfile::isGlobalUserProfile() const
126 {
127 return roomid_ == "";
128 }
129
130 crypto::Trust
getUserStatus()131 UserProfile::getUserStatus()
132 {
133 return isUserVerified;
134 }
135
136 bool
userVerificationEnabled() const137 UserProfile::userVerificationEnabled() const
138 {
139 return hasMasterKey;
140 }
141 bool
isSelf() const142 UserProfile::isSelf() const
143 {
144 return this->userid_ == utils::localUser();
145 }
146
147 void
signOutDevice(const QString & deviceID)148 UserProfile::signOutDevice(const QString &deviceID)
149 {
150 http::client()->delete_device(
151 deviceID.toStdString(),
152 UIA::instance()->genericHandler(tr("Sign out device %1").arg(deviceID)),
153 [this, deviceID](mtx::http::RequestErr e) {
154 if (e) {
155 nhlog::ui()->critical("Failure when attempting to sign out device {}",
156 deviceID.toStdString());
157 return;
158 }
159 nhlog::ui()->info("Device {} successfully signed out!", deviceID.toStdString());
160 // This is us. Let's update the interface accordingly
161 if (isSelf() && deviceID.toStdString() == ::http::client()->device_id()) {
162 ChatPage::instance()->dropToLoginPageCb(tr("You signed out this device."));
163 }
164 refreshDevices();
165 });
166 }
167
168 void
refreshDevices()169 UserProfile::refreshDevices()
170 {
171 cache::client()->markUserKeysOutOfDate({this->userid_.toStdString()});
172 fetchDeviceList(this->userid_);
173 }
174
175 void
fetchDeviceList(const QString & userID)176 UserProfile::fetchDeviceList(const QString &userID)
177 {
178 auto localUser = utils::localUser();
179
180 if (!cache::client() || !cache::client()->isDatabaseReady())
181 return;
182
183 cache::client()->query_keys(
184 userID.toStdString(),
185 [other_user_id = userID.toStdString(), this](const UserKeyCache &,
186 mtx::http::RequestErr err) {
187 if (err) {
188 nhlog::net()->warn("failed to query device keys: {},{}",
189 mtx::errors::to_string(err->matrix_error.errcode),
190 static_cast<int>(err->status_code));
191 return;
192 }
193
194 // Ensure local key cache is up to date
195 cache::client()->query_keys(
196 utils::localUser().toStdString(),
197 [this](const UserKeyCache &, mtx::http::RequestErr err) {
198 using namespace mtx;
199 std::string local_user_id = utils::localUser().toStdString();
200
201 if (err) {
202 nhlog::net()->warn("failed to query device keys: {},{}",
203 mtx::errors::to_string(err->matrix_error.errcode),
204 static_cast<int>(err->status_code));
205 return;
206 }
207
208 emit verificationStatiChanged();
209 });
210 });
211 }
212
213 void
updateVerificationStatus()214 UserProfile::updateVerificationStatus()
215 {
216 if (!cache::client() || !cache::client()->isDatabaseReady())
217 return;
218
219 auto user_keys = cache::client()->userKeys(userid_.toStdString());
220 if (!user_keys) {
221 this->hasMasterKey = false;
222 this->isUserVerified = crypto::Trust::Unverified;
223 this->deviceList_.reset({});
224 emit userStatusChanged();
225 return;
226 }
227
228 this->hasMasterKey = !user_keys->master_keys.keys.empty();
229
230 std::vector<DeviceInfo> deviceInfo;
231 auto devices = user_keys->device_keys;
232 auto verificationStatus = cache::client()->verificationStatus(userid_.toStdString());
233
234 this->isUserVerified = verificationStatus.user_verified;
235 emit userStatusChanged();
236
237 for (const auto &d : devices) {
238 auto device = d.second;
239 verification::Status verified =
240 std::find(verificationStatus.verified_devices.begin(),
241 verificationStatus.verified_devices.end(),
242 device.device_id) == verificationStatus.verified_devices.end()
243 ? verification::UNVERIFIED
244 : verification::VERIFIED;
245
246 if (isSelf() && device.device_id == ::http::client()->device_id())
247 verified = verification::Status::SELF;
248
249 deviceInfo.push_back({QString::fromStdString(d.first),
250 QString::fromStdString(device.unsigned_info.device_display_name),
251 verified});
252 }
253
254 // For self, also query devices without keys
255 if (isSelf()) {
256 http::client()->query_devices(
257 [this, deviceInfo](const mtx::responses::QueryDevices &allDevs,
258 mtx::http::RequestErr err) mutable {
259 if (err) {
260 nhlog::net()->warn("failed to query devices: {} {}",
261 err->matrix_error.error,
262 static_cast<int>(err->status_code));
263 this->deviceList_.queueReset(std::move(deviceInfo));
264 emit devicesChanged();
265 return;
266 }
267 for (const auto &d : allDevs.devices) {
268 // First, check if we already have an entry for this device
269 bool found = false;
270 for (auto &e : deviceInfo) {
271 if (e.device_id.toStdString() == d.device_id) {
272 found = true;
273 // Gottem! Let's fill in the blanks
274 e.lastIp = QString::fromStdString(d.last_seen_ip);
275 e.lastTs = d.last_seen_ts;
276 break;
277 }
278 }
279 // No entry? Let's add one.
280 if (!found) {
281 deviceInfo.push_back({QString::fromStdString(d.device_id),
282 QString::fromStdString(d.display_name),
283 verification::NOT_APPLICABLE,
284 QString::fromStdString(d.last_seen_ip),
285 d.last_seen_ts});
286 }
287 }
288
289 this->deviceList_.queueReset(std::move(deviceInfo));
290 emit devicesChanged();
291 });
292 return;
293 }
294
295 this->deviceList_.queueReset(std::move(deviceInfo));
296 emit devicesChanged();
297 }
298
299 void
banUser()300 UserProfile::banUser()
301 {
302 ChatPage::instance()->banUser(this->userid_, "");
303 }
304
305 // void ignoreUser(){
306
307 // }
308
309 void
kickUser()310 UserProfile::kickUser()
311 {
312 ChatPage::instance()->kickUser(this->userid_, "");
313 }
314
315 void
startChat()316 UserProfile::startChat()
317 {
318 ChatPage::instance()->startChat(this->userid_);
319 }
320
321 void
changeUsername(QString username)322 UserProfile::changeUsername(QString username)
323 {
324 if (isGlobalUserProfile()) {
325 // change global
326 http::client()->set_displayname(username.toStdString(), [](mtx::http::RequestErr err) {
327 if (err) {
328 nhlog::net()->warn("could not change username");
329 return;
330 }
331 });
332 } else {
333 // change room username
334 mtx::events::state::Member member;
335 member.display_name = username.toStdString();
336 member.avatar_url =
337 cache::avatarUrl(roomid_, QString::fromStdString(http::client()->user_id().to_string()))
338 .toStdString();
339 member.membership = mtx::events::state::Membership::Join;
340
341 updateRoomMemberState(std::move(member));
342 }
343 }
344
345 void
changeDeviceName(QString deviceID,QString deviceName)346 UserProfile::changeDeviceName(QString deviceID, QString deviceName)
347 {
348 http::client()->set_device_name(
349 deviceID.toStdString(), deviceName.toStdString(), [this](mtx::http::RequestErr err) {
350 if (err) {
351 nhlog::net()->warn("could not change device name");
352 return;
353 }
354 refreshDevices();
355 });
356 }
357
358 void
verify(QString device)359 UserProfile::verify(QString device)
360 {
361 if (!device.isEmpty())
362 manager->verificationManager()->verifyDevice(userid_, device);
363 else {
364 manager->verificationManager()->verifyUser(userid_);
365 }
366 }
367
368 void
unverify(QString device)369 UserProfile::unverify(QString device)
370 {
371 cache::markDeviceUnverified(userid_.toStdString(), device.toStdString());
372 }
373
374 void
setGlobalUsername(const QString & globalUser)375 UserProfile::setGlobalUsername(const QString &globalUser)
376 {
377 globalUsername = globalUser;
378 emit displayNameChanged();
379 }
380
381 void
changeAvatar()382 UserProfile::changeAvatar()
383 {
384 const QString picturesFolder =
385 QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
386 const QString fileName = QFileDialog::getOpenFileName(
387 nullptr, tr("Select an avatar"), picturesFolder, tr("All Files (*)"));
388
389 if (fileName.isEmpty())
390 return;
391
392 QMimeDatabase db;
393 QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
394
395 const auto format = mime.name().split("/")[0];
396
397 QFile file{fileName, this};
398 if (format != "image") {
399 emit displayError(tr("The selected file is not an image"));
400 return;
401 }
402
403 if (!file.open(QIODevice::ReadOnly)) {
404 emit displayError(tr("Error while reading file: %1").arg(file.errorString()));
405 return;
406 }
407
408 const auto bin = file.peek(file.size());
409 const auto payload = std::string(bin.data(), bin.size());
410
411 isLoading_ = true;
412 emit loadingChanged();
413
414 // First we need to create a new mxc URI
415 // (i.e upload media to the Matrix content repository) for the new avatar.
416 http::client()->upload(
417 payload,
418 mime.name().toStdString(),
419 QFileInfo(fileName).fileName().toStdString(),
420 [this,
421 payload,
422 mimetype = mime.name().toStdString(),
423 size = payload.size(),
424 room_id = roomid_.toStdString(),
425 content = std::move(bin)](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) {
426 if (err) {
427 nhlog::ui()->error("Failed to upload image", err->matrix_error.error);
428 return;
429 }
430
431 if (isGlobalUserProfile()) {
432 http::client()->set_avatar_url(res.content_uri, [this](mtx::http::RequestErr err) {
433 if (err) {
434 nhlog::ui()->error("Failed to set user avatar url", err->matrix_error.error);
435 }
436
437 isLoading_ = false;
438 emit loadingChanged();
439 getGlobalProfileData();
440 });
441 } else {
442 // change room username
443 mtx::events::state::Member member;
444 member.display_name = cache::displayName(roomid_, userid_).toStdString();
445 member.avatar_url = res.content_uri;
446 member.membership = mtx::events::state::Membership::Join;
447
448 updateRoomMemberState(std::move(member));
449 }
450 });
451 }
452
453 void
updateRoomMemberState(mtx::events::state::Member member)454 UserProfile::updateRoomMemberState(mtx::events::state::Member member)
455 {
456 http::client()->send_state_event(
457 roomid_.toStdString(),
458 http::client()->user_id().to_string(),
459 member,
460 [](mtx::responses::EventId, mtx::http::RequestErr err) {
461 if (err)
462 nhlog::net()->error("Failed to update room member state : ", err->matrix_error.error);
463 });
464 }
465
466 void
updateAvatarUrl()467 UserProfile::updateAvatarUrl()
468 {
469 isLoading_ = false;
470 emit loadingChanged();
471
472 emit avatarUrlChanged();
473 }
474
475 bool
isLoading() const476 UserProfile::isLoading() const
477 {
478 return isLoading_;
479 }
480
481 void
getGlobalProfileData()482 UserProfile::getGlobalProfileData()
483 {
484 http::client()->get_profile(
485 userid_.toStdString(), [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
486 if (err) {
487 nhlog::net()->warn("failed to retrieve profile info for {}", userid_.toStdString());
488 return;
489 }
490
491 emit globalUsernameRetrieved(QString::fromStdString(res.display_name));
492 globalAvatarUrl = QString::fromStdString(res.avatar_url);
493 emit avatarUrlChanged();
494 });
495 }
496
497 void
openGlobalProfile()498 UserProfile::openGlobalProfile()
499 {
500 emit manager->openGlobalUserProfile(userid_);
501 }
502