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