1// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
2// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
3// SPDX-License-Identifier: LGPL-2.1-or-later
4
5#include "user.h"
6
7#include "avatar.h"
8#include "connection.h"
9#include "logging_categories_p.h"
10#include "room.h"
11
12#include "csapi/content-repo.h"
13#include "csapi/profile.h"
14#include "csapi/room_state.h"
15
16#include "events/event.h"
17#include "events/roommemberevent.h"
18
19#include <QtCore/QElapsedTimer>
20#include <QtCore/QPointer>
21#include <QtCore/QRegularExpression>
22#include <QtCore/QStringBuilder>
23#include <QtCore/QTimer>
24
25#include <functional>
26
27using namespace Quotient;
28
29class Q_DECL_HIDDEN User::Private {
30public:
31 Private(QString userId) : id(std::move(userId)), hueF(stringToHueF(s: id)) { }
32
33 QString id;
34 qreal hueF;
35
36 QString defaultName;
37 Avatar defaultAvatar;
38 // NB: This container is ever-growing. Even if the user no more scrolls
39 // the timeline that far back, historical avatars are still kept around.
40 // This is consistent with the rest of Quotient, as room timelines
41 // are never vacuumed either. This will probably change in the future.
42 /// Map of mediaId to Avatar objects
43 static UnorderedMap<QString, Avatar> otherAvatars;
44};
45
46decltype(User::Private::otherAvatars) User::Private::otherAvatars {};
47
48User::User(QString userId, Connection* connection)
49 : QObject(connection), d(makeImpl<Private>(args: std::move(userId)))
50{
51 setObjectName(id());
52}
53
54Connection* User::connection() const
55{
56 Q_ASSERT(parent());
57 return static_cast<Connection*>(parent());
58}
59
60void User::load()
61{
62 auto* profileJob =
63 connection()->callApi<GetUserProfileJob>(jobArgs: id());
64 connect(sender: profileJob, signal: &BaseJob::result, context: this, slot: [this, profileJob] {
65 d->defaultName = profileJob->displayname();
66 d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl()));
67 emit defaultNameChanged();
68 emit defaultAvatarChanged();
69 });
70}
71
72QString User::id() const { return d->id; }
73
74bool User::isGuest() const
75{
76 Q_ASSERT(!d->id.isEmpty() && d->id.startsWith(u'@'));
77 auto it = std::find_if_not(first: d->id.cbegin() + 1, last: d->id.cend(),
78 pred: [](QChar c) { return c.isDigit(); });
79 Q_ASSERT(it != d->id.cend());
80 return *it == u':';
81}
82
83int User::hue() const { return int(hueF() * 359); }
84
85QString User::name(const Room* room) const
86{
87 return room ? room->memberName(mxId: id()) : d->defaultName;
88}
89
90void User::rename(const QString& newName)
91{
92 const auto actualNewName = sanitized(plainText: newName);
93 if (actualNewName == d->defaultName)
94 return; // Nothing to do
95
96 connect(sender: connection()->callApi<SetDisplayNameJob>(jobArgs: id(), jobArgs: actualNewName),
97 signal: &BaseJob::success, context: this, slot: [this, actualNewName] {
98 // Check again, it could have changed meanwhile
99 if (actualNewName != d->defaultName) {
100 d->defaultName = actualNewName;
101 emit defaultNameChanged();
102 } else
103 qCWarning(MAIN)
104 << "User" << id() << "already has profile name set to"
105 << actualNewName;
106 });
107}
108
109void User::rename(const QString& newName, Room* r)
110{
111 if (!r) {
112 qCWarning(MAIN) << "Passing a null room to two-argument User::rename()"
113 "is incorrect; client developer, please fix it";
114 rename(newName);
115 return;
116 }
117 // #481: take the current state and update it with the new name
118 if (const auto& maybeEvt = r->currentState().get<RoomMemberEvent>(stateKey: id())) {
119 auto content = maybeEvt->content();
120 if (content.membership == Membership::Join) {
121 content.displayName = sanitized(plainText: newName);
122 r->setState<RoomMemberEvent>(args: id(), args: std::move(content));
123 // The state will be updated locally after it arrives with sync
124 return;
125 }
126 }
127 qCCritical(MEMBERS)
128 << "Attempt to rename a non-member in a room context - ignored";
129}
130
131template <typename SourceT>
132inline bool User::doSetAvatar(SourceT&& source)
133{
134 return d->defaultAvatar.upload(
135 connection(), source, [this](const QUrl& contentUri) {
136 auto* j = connection()->callApi<SetAvatarUrlJob>(jobArgs: id(), jobArgs: contentUri);
137 connect(j, &BaseJob::success, this,
138 [this, contentUri] {
139 if (contentUri == d->defaultAvatar.url()) {
140 d->defaultAvatar.updateUrl(newUrl: contentUri);
141 emit defaultAvatarChanged();
142 } else
143 qCWarning(MAIN) << "User" << id()
144 << "already has avatar URL set to"
145 << contentUri.toDisplayString();
146 });
147 });
148}
149
150bool User::setAvatar(const QString& fileName)
151{
152 return doSetAvatar(source: fileName);
153}
154
155bool User::setAvatar(QIODevice* source)
156{
157 return doSetAvatar(source);
158}
159
160void User::removeAvatar()
161{
162 connection()->callApi<SetAvatarUrlJob>(jobArgs: id(), jobArgs: QUrl());
163}
164
165void User::requestDirectChat() { connection()->requestDirectChat(u: this); }
166
167void User::ignore() { connection()->addToIgnoredUsers(user: this); }
168
169void User::unmarkIgnore() { connection()->removeFromIgnoredUsers(user: this); }
170
171bool User::isIgnored() const { return connection()->isIgnored(user: this); }
172
173QString User::displayname(const Room* room) const
174{
175 return room ? room->safeMemberName(userId: id())
176 : d->defaultName.isEmpty() ? d->id : d->defaultName;
177}
178
179QString User::fullName(const Room* room) const
180{
181 const auto displayName = name(room);
182 return displayName.isEmpty() ? id() : (displayName % " ("_ls % id() % u')');
183}
184
185const Avatar& User::avatarObject(const Room* room) const
186{
187 if (!room)
188 return d->defaultAvatar;
189
190 const auto& url = room->memberAvatarUrl(mxId: id());
191 const auto& mediaId = url.authority() + url.path();
192 return d->otherAvatars.try_emplace(k: mediaId, args: url).first->second;
193}
194
195QImage User::avatar(int dimension, const Room* room) const
196{
197 return avatar(requestedWidth: dimension, requestedHeight: dimension, room);
198}
199
200QImage User::avatar(int width, int height, const Room* room) const
201{
202 return avatar(width, height, room, callback: [] {});
203}
204
205QImage User::avatar(int width, int height, const Room* room,
206 const Avatar::get_callback_t& callback) const
207{
208 return avatarObject(room).get(connection: connection(), w: width, h: height, callback);
209}
210
211QString User::avatarMediaId(const Room* room) const
212{
213 return avatarObject(room).mediaId();
214}
215
216QUrl User::avatarUrl(const Room* room) const
217{
218 return avatarObject(room).url();
219}
220
221qreal User::hueF() const { return d->hueF; }
222