aboutsummaryrefslogtreecommitdiff
path: root/src/client/QXmppAttentionManager.cpp
blob: 55bdc17979e4bf1809f64a6fbe4b71c517928206 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// SPDX-FileCopyrightText: 2020 Linus Jahn <lnj@kaidan.im>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "QXmppAttentionManager.h"

// Qt
#include <QTimer>
// 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<PastRequest> 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<QXmppRosterManager>()) {
        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);
        }
    }
}