diff options
author | Andres Salomon <dilinger@queued.net> | 2021-01-18 04:00:14 -0500 |
---|---|---|
committer | Andres Salomon <dilinger@queued.net> | 2021-01-18 04:00:14 -0500 |
commit | 09eb39236666e81d5da014acea011dcd74d0999b (patch) | |
tree | 52876d96be71be1a39d5d935c1295a51995e8949 /lib/user.cpp | |
parent | f1788ee27f33e9339334e0d79bde9a27d9ce2e44 (diff) | |
parent | a4e78956f105875625b572d8b98459ffa86fafe5 (diff) | |
download | libquotient-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.cpp | 462 |
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; } |