// SPDX-FileCopyrightText: 2020 Linus Jahn // // SPDX-License-Identifier: LGPL-2.1-or-later #include "QXmppAttentionManager.h" // Qt #include // QXmpp #include "QXmppClient.h" #include "QXmppConstants_p.h" #include "QXmppMessage.h" #include "QXmppRosterManager.h" #include "QXmppUtils.h" /// /// \class QXmppAttentionManager /// /// \brief The QXmppAttentionManager class manages attention requests as defined /// by \xep{0224}: Attention. /// /// The manager also does some checks, including rate limiting and checking /// whether the senders are trusted (aka. in the roster). /// /// Rate limited messages are not emitted on the normal attentionRequested() /// signal and are sent on the attentionRequestRateLimited() signal instead. /// /// To use this manager you still need to instantiate it and register it with /// the QXmppClient: /// /// \code /// auto *attentionManager = new QXmppAttentionManager(); /// client->addExtension(attentionManager); /// \endcode /// /// \since QXmpp 1.4 /// /// /// \fn QXmppAttentionManager::attentionRequested /// /// This signal is emitted when an attention request was received and it passed /// the rate limiter. /// /// \param message The message with the attention request that was received. /// \param isTrusted Whether the sender of the message exists in the user's /// roster. /// /// /// \fn QXmppAttentionManager::attentionRequestRateLimited /// /// This signal is emitted when an attention request did not pass the rate /// limiter. /// /// \param message The message with the attention request that did not pass the /// rate limiter. /// struct PastRequest { QString bareJid; QDateTime timestamp; }; class QXmppAttentionManagerPrivate : public QObject { public: QXmppAttentionManagerPrivate(QXmppAttentionManager *parent, quint8 allowedAttempts, QTime timeFrame); bool checkRateLimit(const QString &bareJid); void cleanUp(); quint8 allowedAttempts; QTime allowedAttemptsTimeInterval; // map of bare JIDs and time of previous requests QVector previousRequests; QTimer *cleanUpTimer; }; /// /// \brief QXmppAttentionManager::QXmppAttentionManager /// \param allowedAttempts /// \param timeFrame /// QXmppAttentionManager::QXmppAttentionManager(quint8 allowedAttempts, QTime timeFrame) : d(new QXmppAttentionManagerPrivate(this, allowedAttempts, timeFrame)) { } QXmppAttentionManager::~QXmppAttentionManager() = default; /// /// Returns the \xep{0224}: Attention feature. /// QStringList QXmppAttentionManager::discoveryFeatures() const { return { ns_attention }; } /// /// Returns the number of allowed attempts of attentions from a bare JID in the /// set time frame. /// /// \sa setAllowedAttempts() /// \sa allowedAttemptsTimeInterval() /// \sa setAllowedAttemptsTimeInterval() /// quint8 QXmppAttentionManager::allowedAttempts() const { return d->allowedAttempts; } /// /// Sets the number of allowed attempts of attentions from a bare JID in the set /// time frame. /// /// \sa allowedAttempts() /// \sa allowedAttemptsTimeInterval() /// \sa setAllowedAttemptsTimeInterval() /// void QXmppAttentionManager::setAllowedAttempts(quint8 allowedAttempts) { d->allowedAttempts = allowedAttempts; } /// /// Returns the time interval for the allowed attempts for rate limiting. /// /// \sa setAllowedAttemptsTimeInterval() /// \sa allowedAttempts() /// \sa setAllowedAttemptsTimeInterval() /// QTime QXmppAttentionManager::allowedAttemptsTimeInterval() const { return d->allowedAttemptsTimeInterval; } /// /// Returns the time interval for the allowed attempts for rate limiting. /// /// \sa allowedAttemptsTimeInterval() /// \sa allowedAttempts() /// \sa setAllowedAttempts() /// void QXmppAttentionManager::setAllowedAttemptsTimeInterval(QTime interval) { d->allowedAttemptsTimeInterval = interval; } /// /// Sends a message of type chat with an attention request to the specified JID. /// /// \xep{0224} allows to include other elements with an attention request, but /// the QXmppAttentionManager has no method for this purpose. However, such a /// request can still be made manually. /// /// \param jid The address to which the request should be sent. /// \param message The message body to include in the attention request. /// /// \return The ID of the sent message, if sent successfully, a null string /// otherwise. In case an ID is returned, it also corresponds to the origin ID /// of the message as defined by \xep{0359}: Unique and Stable Stanza IDs. /// QString QXmppAttentionManager::requestAttention(const QString &jid, const QString &message) { QXmppMessage msg; // The XEP recommends to use type headline, but the message body might still // be of interest later, so we use type chat to allow caching. msg.setType(QXmppMessage::Chat); msg.setId(QXmppUtils::generateStanzaUuid()); msg.setOriginId(msg.id()); msg.setTo(jid); msg.setBody(message); msg.setAttentionRequested(true); if (client()->sendPacket(msg)) { return msg.id(); } return {}; } void QXmppAttentionManager::setClient(QXmppClient *client) { QXmppClientExtension::setClient(client); connect(client, &QXmppClient::messageReceived, this, &QXmppAttentionManager::handleMessageReceived); } void QXmppAttentionManager::handleMessageReceived(const QXmppMessage &message) { if (!message.isAttentionRequested() || !message.stamp().isNull()) { return; } const QString bareJid = QXmppUtils::jidToBareJid(message.from()); // ignore messages from our own bare JID (e.g. carbon or IM-NG message) if (bareJid == client()->configuration().jidBare()) { return; } // check rate limit if (!d->checkRateLimit(bareJid)) { Q_EMIT attentionRequestRateLimited(message); return; } bool isTrusted = false; if (auto *rosterManager = client()->findExtension()) { isTrusted = rosterManager->getRosterBareJids().contains(bareJid); } Q_EMIT attentionRequested(message, isTrusted); } QXmppAttentionManagerPrivate::QXmppAttentionManagerPrivate(QXmppAttentionManager *parent, quint8 allowedAttempts, QTime timeFrame) : allowedAttempts(allowedAttempts), allowedAttemptsTimeInterval(timeFrame), cleanUpTimer(new QTimer(parent)) { QObject::connect(cleanUpTimer, &QTimer::timeout, [this]() { cleanUp(); }); } /// /// Returns true if the request passes the rate limit. /// bool QXmppAttentionManagerPrivate::checkRateLimit(const QString &bareJid) { // add to request to cache previousRequests << PastRequest { bareJid, QDateTime::currentDateTimeUtc() }; // start timer to remove request again if (!cleanUpTimer->isActive()) { cleanUpTimer->start(allowedAttemptsTimeInterval.msecsSinceStartOfDay()); } // check whether there are too many requests int count = std::count_if(previousRequests.cbegin(), previousRequests.cend(), [=](const PastRequest &request) { return request.bareJid == bareJid; }); return count <= allowedAttempts; } /// /// Removes the first entry and reschedules the timer to remove the next. /// void QXmppAttentionManagerPrivate::cleanUp() { previousRequests.removeFirst(); if (!previousRequests.isEmpty()) { // reschedule timer for next removal int next = allowedAttemptsTimeInterval.msecsSinceStartOfDay() - previousRequests.first().timestamp.msecsTo(QDateTime::currentDateTimeUtc()); if (next < 1) { cleanUp(); } else { cleanUpTimer->start(next); } } }