aboutsummaryrefslogtreecommitdiff
path: root/lib/user.cpp
diff options
context:
space:
mode:
authorAndres Salomon <dilinger@queued.net>2021-01-18 04:00:14 -0500
committerAndres Salomon <dilinger@queued.net>2021-01-18 04:00:14 -0500
commit09eb39236666e81d5da014acea011dcd74d0999b (patch)
tree52876d96be71be1a39d5d935c1295a51995e8949 /lib/user.cpp
parentf1788ee27f33e9339334e0d79bde9a27d9ce2e44 (diff)
parenta4e78956f105875625b572d8b98459ffa86fafe5 (diff)
downloadlibquotient-09eb39236666e81d5da014acea011dcd74d0999b.tar.gz
libquotient-09eb39236666e81d5da014acea011dcd74d0999b.zip
Update upstream source from tag 'upstream/0.6.4'
Update to upstream version '0.6.4' with Debian dir aa8705fd74743e79c043bc9e3e425d5064404cfe
Diffstat (limited to 'lib/user.cpp')
-rw-r--r--lib/user.cpp462
1 files changed, 179 insertions, 283 deletions
diff --git a/lib/user.cpp b/lib/user.cpp
index c51354a0..85f9d9a7 100644
--- a/lib/user.cpp
+++ b/lib/user.cpp
@@ -13,274 +13,159 @@
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "user.h"
+#include "avatar.h"
#include "connection.h"
#include "room.h"
-#include "avatar.h"
+
+#include "csapi/content-repo.h"
+#include "csapi/profile.h"
+#include "csapi/room_state.h"
+
#include "events/event.h"
#include "events/roommemberevent.h"
-#include "csapi/room_state.h"
-#include "csapi/profile.h"
-#include "csapi/content-repo.h"
-#include <QtCore/QTimer>
-#include <QtCore/QRegularExpression>
+#include <QtCore/QElapsedTimer>
#include <QtCore/QPointer>
+#include <QtCore/QRegularExpression>
#include <QtCore/QStringBuilder>
-#include <QtCore/QElapsedTimer>
+#include <QtCore/QTimer>
#include <functional>
-using namespace QMatrixClient;
-using namespace std::placeholders;
+using namespace Quotient;
using std::move;
-class User::Private
-{
- public:
- static Avatar makeAvatar(QUrl url)
- {
- return Avatar(move(url));
- }
+class User::Private {
+public:
+ Private(QString userId) : id(move(userId)), hueF(stringToHueF(id)) { }
- Private(QString userId, Connection* connection)
- : userId(move(userId)), connection(connection)
- { }
-
- QString userId;
- Connection* connection;
-
- QString bridged;
- QString mostUsedName;
- QMultiHash<QString, const Room*> otherNames;
- Avatar mostUsedAvatar { makeAvatar({}) };
- std::vector<Avatar> otherAvatars;
- auto otherAvatar(const QUrl& url)
- {
- return std::find_if(otherAvatars.begin(), otherAvatars.end(),
- [&url] (const auto& av) { return av.url() == url; });
- }
- QMultiHash<QUrl, const Room*> avatarsToRooms;
+ QString id;
+ qreal hueF;
- mutable int totalRooms = 0;
+ // In the following two, isNull/nullopt mean they are uninitialised;
+ // isEmpty/Avatar::url().isEmpty() mean they are initialised but empty.
+ QString defaultName;
+ std::optional<Avatar> defaultAvatar;
- QString nameForRoom(const Room* r, const QString& hint = {}) const;
- void setNameForRoom(const Room* r, QString newName, const QString& oldName);
- QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const;
- void setAvatarForRoom(const Room* r, const QUrl& newUrl,
- const QUrl& oldUrl);
+ // NB: This container is ever-growing. Even if the user no more scrolls
+ // the timeline that far back, historical avatars are still kept around.
+ // This is consistent with the rest of Quotient, as room timelines
+ // are never rotated either. This will probably change in the future.
+ /// Map of mediaId to Avatar objects
+ static UnorderedMap<QString, Avatar> otherAvatars;
- void setAvatarOnServer(QString contentUri, User* q);
+ void fetchProfile(const User* q);
+ template <typename SourceT>
+ bool doSetAvatar(SourceT&& source, User* q);
};
+decltype(User::Private::otherAvatars) User::Private::otherAvatars {};
-QString User::Private::nameForRoom(const Room* r, const QString& hint) const
+void User::Private::fetchProfile(const User* q)
{
- // If the hint is accurate, this function is O(1) instead of O(n)
- if (!hint.isNull()
- && (hint == mostUsedName || otherNames.contains(hint, r)))
- return hint;
- return otherNames.key(r, mostUsedName);
-}
-
-static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20;
-
-void User::Private::setNameForRoom(const Room* r, QString newName,
- const QString& oldName)
-{
- Q_ASSERT(oldName != newName);
- Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r));
- if (totalRooms < 2)
- {
- Q_ASSERT_X(totalRooms > 0 && otherNames.empty(), __FUNCTION__,
- "Internal structures inconsistency");
- mostUsedName = move(newName);
- return;
- }
- otherNames.remove(oldName, r);
- if (newName != mostUsedName)
- {
- // Check if the newName is about to become most used.
- if (otherNames.count(newName) >= totalRooms - otherNames.size())
- {
- Q_ASSERT(totalRooms > 1);
- QElapsedTimer et;
- if (totalRooms > MIN_JOINED_ROOMS_TO_LOG)
- {
- qCDebug(MAIN) << "Switching the most used name of user" << userId
- << "from" << mostUsedName << "to" << newName;
- qCDebug(MAIN) << "The user is in" << totalRooms << "rooms";
- et.start();
- }
-
- for (auto* r1: connection->allRooms())
- if (nameForRoom(r1) == mostUsedName)
- otherNames.insert(mostUsedName, r1);
-
- mostUsedName = newName;
- otherNames.remove(newName);
- if (totalRooms > MIN_JOINED_ROOMS_TO_LOG)
- qCDebug(PROFILER) << et << "to switch the most used name";
- }
- else
- otherNames.insert(newName, r);
- }
-}
-
-QUrl User::Private::avatarUrlForRoom(const Room* r, const QUrl& hint) const
-{
- // If the hint is accurate, this function is O(1) instead of O(n)
- if (hint == mostUsedAvatar.url() || avatarsToRooms.contains(hint, r))
- return hint;
- auto it = std::find(avatarsToRooms.begin(), avatarsToRooms.end(), r);
- return it == avatarsToRooms.end() ? mostUsedAvatar.url() : it.key();
-}
-
-void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl,
- const QUrl& oldUrl)
-{
- Q_ASSERT(oldUrl != newUrl);
- Q_ASSERT(oldUrl == mostUsedAvatar.url() ||
- avatarsToRooms.contains(oldUrl, r));
- if (totalRooms < 2)
- {
- Q_ASSERT_X(totalRooms > 0 && otherAvatars.empty(), __FUNCTION__,
- "Internal structures inconsistency");
- mostUsedAvatar.updateUrl(newUrl);
- return;
- }
- avatarsToRooms.remove(oldUrl, r);
- if (!avatarsToRooms.contains(oldUrl))
- {
- auto it = otherAvatar(oldUrl);
- if (it != otherAvatars.end())
- otherAvatars.erase(it);
- }
- if (newUrl != mostUsedAvatar.url())
- {
- // Check if the new avatar is about to become most used.
- const auto newUrlUsage = avatarsToRooms.count(newUrl);
- if (newUrlUsage >= totalRooms - avatarsToRooms.size()) {
- QElapsedTimer et;
- if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) {
- qCInfo(MAIN) << "Switching the most used avatar of user" << userId
- << "from" << mostUsedAvatar.url().toDisplayString()
- << "to" << newUrl.toDisplayString();
- et.start();
- }
- avatarsToRooms.remove(newUrl);
- auto nextMostUsedIt = otherAvatar(newUrl);
- if (nextMostUsedIt == otherAvatars.end()) {
- qCCritical(MAIN)
- << userId << "doesn't have" << newUrl.toDisplayString()
- << "in otherAvatars though it seems to be used in"
- << newUrlUsage << "rooms";
- Q_ASSERT(false);
- otherAvatars.emplace_back(makeAvatar(newUrl));
- nextMostUsedIt = otherAvatars.end() - 1;
- }
- std::swap(mostUsedAvatar, *nextMostUsedIt);
- for (const auto* r1: connection->allRooms())
- if (avatarUrlForRoom(r1) == nextMostUsedIt->url())
- avatarsToRooms.insert(nextMostUsedIt->url(), r1);
-
- if (totalRooms > MIN_JOINED_ROOMS_TO_LOG)
- qCDebug(PROFILER) << et << "to switch the most used avatar";
- } else {
- if (otherAvatar(newUrl) == otherAvatars.end())
- otherAvatars.emplace_back(makeAvatar(newUrl));
- avatarsToRooms.insert(newUrl, r);
- }
- }
+ defaultAvatar.emplace(Avatar {});
+ defaultName = "";
+ auto* j = q->connection()->callApi<GetUserProfileJob>(BackgroundRequest, id);
+ // FIXME: accepting const User* and const_cast'ing it here is only
+ // until we get a better User API in 0.7
+ QObject::connect(j, &BaseJob::success, q,
+ [this, q = const_cast<User*>(q), j] {
+ q->updateName(j->displayname());
+ defaultAvatar->updateUrl(j->avatarUrl());
+ emit q->avatarChanged(q, nullptr);
+ });
}
User::User(QString userId, Connection* connection)
- : QObject(connection), d(new Private(move(userId), connection))
+ : QObject(connection), d(new Private(move(userId)))
{
- setObjectName(userId);
+ setObjectName(id());
}
Connection* User::connection() const
{
- Q_ASSERT(d->connection);
- return d->connection;
+ Q_ASSERT(parent());
+ return static_cast<Connection*>(parent());
}
User::~User() = default;
-QString User::id() const
-{
- return d->userId;
-}
+QString User::id() const { return d->id; }
bool User::isGuest() const
{
- Q_ASSERT(!d->userId.isEmpty() && d->userId.startsWith('@'));
- auto it = std::find_if_not(d->userId.begin() + 1, d->userId.end(),
- [] (QChar c) { return c.isDigit(); });
- Q_ASSERT(it != d->userId.end());
+ Q_ASSERT(!d->id.isEmpty() && d->id.startsWith('@'));
+ auto it = std::find_if_not(d->id.cbegin() + 1, d->id.cend(),
+ [](QChar c) { return c.isDigit(); });
+ Q_ASSERT(it != d->id.end());
return *it == ':';
}
-QString User::name(const Room* room) const
-{
- return d->nameForRoom(room);
-}
+int User::hue() const { return int(hueF() * 359); }
-QString User::rawName(const Room* room) const
+/// \sa https://github.com/matrix-org/matrix-doc/issues/1375
+///
+/// Relies on untrusted prevContent so can't be put to RoomMemberEvent and
+/// in general should rather be remade in terms of the room's eventual "state
+/// time machine"
+QString getBestKnownName(const RoomMemberEvent* event)
{
- return d->bridged.isEmpty() ? name(room) :
- name(room) % " (" % d->bridged % ')';
+ const auto& jv = event->contentJson().value("displayname"_ls);
+ return !jv.isUndefined()
+ ? jv.toString()
+ : event->prevContent() ? event->prevContent()->displayName
+ : QString();
}
-void User::updateName(const QString& newName, const Room* room)
+QString User::name(const Room* room) const
{
- updateName(newName, d->nameForRoom(room), room);
-}
+ if (room)
+ return getBestKnownName(room->getCurrentState<RoomMemberEvent>(id()));
-void User::updateName(const QString& newName, const QString& oldName,
- const Room* room)
-{
- Q_ASSERT(oldName == d->mostUsedName || d->otherNames.contains(oldName, room));
- if (newName != oldName)
- {
- emit nameAboutToChange(newName, oldName, room);
- d->setNameForRoom(room, newName, oldName);
- setObjectName(displayname());
- emit nameChanged(newName, oldName, room);
- }
+ if (d->defaultName.isNull())
+ d->fetchProfile(this);
+
+ return d->defaultName;
}
-void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl,
- const Room* room)
+QString User::rawName(const Room* room) const { return name(room); }
+
+void User::updateName(const QString& newName, const Room* r)
{
- Q_ASSERT(oldUrl == d->mostUsedAvatar.url() ||
- d->avatarsToRooms.contains(oldUrl, room));
- if (newUrl != oldUrl)
- {
- d->setAvatarForRoom(room, newUrl, oldUrl);
- setObjectName(displayname());
- emit avatarChanged(this, room);
- }
+ Q_ASSERT(r == nullptr);
+ if (newName == d->defaultName)
+ return;
+ emit nameAboutToChange(newName, d->defaultName, nullptr);
+ const auto& oldName =
+ std::exchange(d->defaultName, newName);
+ emit nameChanged(d->defaultName, oldName, nullptr);
}
+void User::updateName(const QString&, const QString&, const Room*) {}
+void User::updateAvatarUrl(const QUrl&, const QUrl&, const Room*) {}
void User::rename(const QString& newName)
{
const auto actualNewName = sanitized(newName);
+ if (actualNewName == d->defaultName)
+ return; // Nothing to do
+
connect(connection()->callApi<SetDisplayNameJob>(id(), actualNewName),
- &BaseJob::success, this, [=] { updateName(actualNewName); });
+ &BaseJob::success, this, [this, actualNewName] {
+ d->fetchProfile(this);
+ updateName(actualNewName);
+ });
}
void User::rename(const QString& newName, const Room* r)
{
- if (!r)
- {
+ if (!r) {
qCWarning(MAIN) << "Passing a null room to two-argument User::rename()"
"is incorrect; client developer, please fix it";
rename(newName);
@@ -291,73 +176,99 @@ void User::rename(const QString& newName, const Room* r)
const auto actualNewName = sanitized(newName);
MemberEventContent evtC;
evtC.displayName = actualNewName;
- connect(r->setMemberState(id(), RoomMemberEvent(move(evtC))),
- &BaseJob::success, this, [=] { updateName(actualNewName, r); });
+ r->setState<RoomMemberEvent>(id(), move(evtC));
+ // The state will be updated locally after it arrives with sync
}
-bool User::setAvatar(const QString& fileName)
+template <typename SourceT>
+bool User::Private::doSetAvatar(SourceT&& source, User* q)
{
- return avatarObject().upload(connection(), fileName,
- std::bind(&Private::setAvatarOnServer, d.data(), _1, this));
+ if (!defaultAvatar) {
+ defaultName = "";
+ defaultAvatar.emplace(Avatar {});
+ }
+ return defaultAvatar->upload(
+ q->connection(), source, [this, q](const QString& contentUri) {
+ auto* j =
+ q->connection()->callApi<SetAvatarUrlJob>(id, contentUri);
+ QObject::connect(j, &BaseJob::success, q,
+ [this, q, newUrl = QUrl(contentUri)] {
+ // Fetch displayname to complete the profile
+ fetchProfile(q);
+ if (newUrl == defaultAvatar->url()) {
+ qCWarning(MAIN)
+ << "User" << id
+ << "already has avatar URL set to"
+ << newUrl.toDisplayString();
+ return;
+ }
+
+ defaultAvatar->updateUrl(newUrl);
+ emit q->avatarChanged(q, nullptr);
+ });
+ });
}
-bool User::setAvatar(QIODevice* source)
+bool User::setAvatar(const QString& fileName)
{
- return avatarObject().upload(connection(), source,
- std::bind(&Private::setAvatarOnServer, d.data(), _1, this));
+ return d->doSetAvatar(fileName, this);
}
-void User::requestDirectChat()
+bool User::setAvatar(QIODevice* source)
{
- connection()->requestDirectChat(this);
+ return d->doSetAvatar(source, this);
}
-void User::ignore()
-{
- connection()->addToIgnoredUsers(this);
-}
+void User::requestDirectChat() { connection()->requestDirectChat(this); }
-void User::unmarkIgnore()
-{
- connection()->removeFromIgnoredUsers(this);
-}
+void User::ignore() { connection()->addToIgnoredUsers(this); }
-bool User::isIgnored() const
-{
- return connection()->isIgnored(this);
-}
+void User::unmarkIgnore() { connection()->removeFromIgnoredUsers(this); }
-void User::Private::setAvatarOnServer(QString contentUri, User* q)
-{
- auto* j = connection->callApi<SetAvatarUrlJob>(userId, contentUri);
- connect(j, &BaseJob::success, q,
- [=] { q->updateAvatarUrl(contentUri, avatarUrlForRoom(nullptr)); });
-}
+bool User::isIgnored() const { return connection()->isIgnored(this); }
QString User::displayname(const Room* room) const
{
if (room)
return room->roomMembername(this);
- const auto name = d->nameForRoom(nullptr);
- return name.isEmpty() ? d->userId : name;
+ if (auto n = name(); !n.isEmpty())
+ return n;
+
+ return d->id;
}
QString User::fullName(const Room* room) const
{
- const auto name = d->nameForRoom(room);
- return name.isEmpty() ? d->userId : name % " (" % d->userId % ')';
+ const auto displayName = name(room);
+ return displayName.isEmpty() ? id() : (displayName % " (" % id() % ')');
}
-QString User::bridged() const
+QString User::bridged() const { return {}; }
+
+/// \sa getBestKnownName, https://github.com/matrix-org/matrix-doc/issues/1375
+QUrl getBestKnownAvatarUrl(const RoomMemberEvent* event)
{
- return d->bridged;
+ const auto& jv = event->contentJson().value("avatar_url"_ls);
+ return !jv.isUndefined()
+ ? jv.toString()
+ : event->prevContent() ? event->prevContent()->avatarUrl
+ : QUrl();
}
const Avatar& User::avatarObject(const Room* room) const
{
- auto it = d->otherAvatar(d->avatarUrlForRoom(room));
- return it != d->otherAvatars.end() ? *it : d->mostUsedAvatar;
+ if (!room) {
+ if (!d->defaultAvatar) {
+ d->fetchProfile(this);
+ }
+ return *d->defaultAvatar;
+ }
+
+ const auto& url =
+ getBestKnownAvatarUrl(room->getCurrentState<RoomMemberEvent>(id()));
+ const auto& mediaId = url.authority() + url.path();
+ return d->otherAvatars.try_emplace(mediaId, url).first->second;
}
QImage User::avatar(int dimension, const Room* room)
@@ -367,14 +278,16 @@ QImage User::avatar(int dimension, const Room* room)
QImage User::avatar(int width, int height, const Room* room)
{
- return avatar(width, height, room, []{});
+ return avatar(width, height, room, [] {});
}
QImage User::avatar(int width, int height, const Room* room,
const Avatar::get_callback_t& callback)
{
- return avatarObject(room).get(d->connection, width, height,
- [=] { emit avatarChanged(this, room); callback(); });
+ return avatarObject(room).get(connection(), width, height, [=] {
+ emit avatarChanged(this, room);
+ callback();
+ });
}
QString User::avatarMediaId(const Room* room) const
@@ -392,44 +305,27 @@ void User::processEvent(const RoomMemberEvent& event, const Room* room,
{
Q_ASSERT(room);
- if (firstMention)
- ++d->totalRooms;
-
- if (event.membership() != MembershipType::Invite &&
- event.membership() != MembershipType::Join)
- return;
-
- auto newName = event.displayName();
- // `bridged` value uses the same notification signal as the name;
- // it is assumed that first setting of the bridge occurs together with
- // the first setting of the name, and further bridge updates are
- // exceptionally rare (the only reasonable case being that the bridge
- // changes the naming convention). For the same reason room-specific
- // bridge tags are not supported at all.
- QRegularExpression reSuffix(QStringLiteral(" \\((IRC|Gitter|Telegram)\\)$"));
- auto match = reSuffix.match(newName);
- if (match.hasMatch())
- {
- if (d->bridged != match.captured(1))
- {
- if (!d->bridged.isEmpty())
- qCWarning(MAIN) << "Bridge for user" << id() << "changed:"
- << d->bridged << "->" << match.captured(1);
- d->bridged = match.captured(1);
- }
- newName.truncate(match.capturedStart(0));
- }
- if (event.prevContent())
- {
- // FIXME: the hint doesn't work for bridged users
- auto oldNameHint =
- d->nameForRoom(room, event.prevContent()->displayName);
- updateName(newName, oldNameHint, room);
- updateAvatarUrl(event.avatarUrl(),
- d->avatarUrlForRoom(room, event.prevContent()->avatarUrl),
- room);
- } else {
- updateName(newName, room);
- updateAvatarUrl(event.avatarUrl(), d->avatarUrlForRoom(room), room);
+ // This is prone to abuse if prevContent is forged; only here until 0.7
+ // (and the whole method, actually).
+ const auto& oldName = event.prevContent() ? event.prevContent()->displayName
+ : QString();
+ const auto& newName = getBestKnownName(&event);
+ // A hacky way to find out if it's about to change or already changed;
+ // making it a lambda allows to omit stub event creation when unneeded
+ const auto& isAboutToChange = [&event, room, this] {
+ return room->getCurrentState<RoomMemberEvent>(id()) != &event;
+ };
+ if (firstMention || newName != oldName) {
+ if (isAboutToChange())
+ emit nameAboutToChange(newName, oldName, room);
+ else
+ emit nameChanged(newName, oldName, room);
}
+ const auto& oldAvatarUrl =
+ event.prevContent() ? event.prevContent()->avatarUrl : QUrl();
+ const auto& newAvatarUrl = getBestKnownAvatarUrl(&event);
+ if ((firstMention || newAvatarUrl != oldAvatarUrl) && !isAboutToChange())
+ emit avatarChanged(this, room);
}
+
+qreal User::hueF() const { return d->hueF; }