common: Port HighlightRule to ExpressionMatch
[quassel.git] / src / common / highlightrulemanager.h
1 /***************************************************************************
2  *   Copyright (C) 2005-2018 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #pragma once
22
23 #include <utility>
24
25 #include <QString>
26 #include <QStringList>
27 #include <QVariantList>
28 #include <QVariantMap>
29
30 #include "expressionmatch.h"
31 #include "message.h"
32 #include "syncableobject.h"
33
34 class HighlightRuleManager : public SyncableObject
35 {
36     SYNCABLE_OBJECT
37     Q_OBJECT
38
39     Q_PROPERTY(int highlightNick READ highlightNick WRITE setHighlightNick)
40     Q_PROPERTY(bool nicksCaseSensitive READ nicksCaseSensitive WRITE setNicksCaseSensitive)
41
42 public:
43     enum HighlightNickType {
44         NoNick = 0x00,
45         CurrentNick = 0x01,
46         AllNicks = 0x02
47     };
48
49     inline HighlightRuleManager(QObject *parent = nullptr) : SyncableObject(parent) { setAllowClientUpdates(true); }
50     HighlightRuleManager &operator=(const HighlightRuleManager &other);
51
52     /**
53      * Individual highlight rule
54      */
55     class HighlightRule
56     {
57     public:
58         /**
59          * Construct an empty highlight rule
60          */
61         HighlightRule() {}
62
63         /**
64          * Construct a highlight rule with the given parameters
65          *
66          * @param id               Integer ID of the rule
67          * @param contents         String representing a message contents expression to match
68          * @param isRegEx          True if regular expression, otherwise false
69          * @param isCaseSensitive  True if case sensitive, otherwise false
70          * @param isEnabled        True if enabled, otherwise false
71          * @param isInverse        True if rule is treated as highlight ignore, otherwise false
72          * @param sender           String representing a message sender expression to match
73          * @param chanName         String representing a channel name expression to match
74          */
75         HighlightRule(int id, QString contents, bool isRegEx, bool isCaseSensitive, bool isEnabled,
76                       bool isInverse, QString sender, QString chanName)
77             : _id(id), _contents(contents), _isRegEx(isRegEx), _isCaseSensitive(isCaseSensitive),
78               _isEnabled(isEnabled), _isInverse(isInverse), _sender(sender), _chanName(chanName)
79         {
80             _cacheInvalid = true;
81             // Cache expression matches on construction
82             //
83             // This provides immediate feedback on errors when loading the rule.  If profiling shows
84             // this as a performance bottleneck, this can be removed in deference to caching on
85             // first use.
86             //
87             // Inversely, if needed for validity checks, caching can be done on every update below
88             // instead of on first use.
89             determineExpressions();
90         }
91
92         /**
93          * Gets the unique ID of this rule
94          *
95          * @return Integer ID of the rule
96          */
97         inline int id() const {
98             return _id;
99         }
100         /**
101          * Sets the ID of this rule
102          *
103          * CAUTION: IDs should be kept unique!
104          *
105          * @param id Integer ID of the rule
106          */
107         inline void setId(int id) {
108             _id = id;
109         }
110
111         /**
112          * Gets the message contents this rule matches
113          *
114          * NOTE: Use HighlightRule::contentsMatcher() for performing matches
115          *
116          * @return String representing a phrase or expression to match
117          */
118         inline QString contents() const {
119             return _contents;
120         }
121         /**
122          * Sets the message contents this rule matches
123          *
124          * @param contents String representing a phrase or expression to match
125          */
126         inline void setContents(const QString &contents) {
127             _contents = contents;
128             _cacheInvalid = true;
129         }
130
131         /**
132          * Gets if this is a regular expression rule
133          *
134          * @return True if regular expression, otherwise false
135          */
136         inline bool isRegEx() const {
137             return _isRegEx;
138         }
139         /**
140          * Sets if this rule is a regular expression rule
141          *
142          * @param isRegEx True if regular expression, otherwise false
143          */
144         inline void setIsRegEx(bool isRegEx) {
145             _isRegEx = isRegEx;
146             _cacheInvalid = true;
147         }
148
149         /**
150          * Gets if this rule is case sensitive
151          *
152          * @return True if case sensitive, otherwise false
153          */
154         inline bool isCaseSensitive() const {
155             return _isCaseSensitive;
156         }
157         /**
158          * Sets if this rule is case sensitive
159          *
160          * @param isCaseSensitive True if case sensitive, otherwise false
161          */
162         inline void setIsCaseSensitive(bool isCaseSensitive) {
163             _isCaseSensitive = isCaseSensitive;
164             _cacheInvalid = true;
165         }
166
167         /**
168          * Gets if this rule is enabled and active
169          *
170          * @return True if enabled, otherwise false
171          */
172         inline bool isEnabled() const {
173             return _isEnabled;
174         }
175         /**
176          * Sets if this rule is enabled and active
177          *
178          * @param isEnabled True if enabled, otherwise false
179          */
180         inline void setIsEnabled(bool isEnabled) {
181             _isEnabled = isEnabled;
182         }
183
184         /**
185          * Gets if this rule is a highlight ignore rule
186          *
187          * @return True if rule is treated as highlight ignore, otherwise false
188          */
189         inline bool isInverse() const {
190             return _isInverse;
191         }
192         /**
193          * Sets if this rule is a highlight ignore rule
194          *
195          * @param isInverse True if rule is treated as highlight ignore, otherwise false
196          */
197         inline void setIsInverse(bool isInverse) {
198             _isInverse = isInverse;
199         }
200
201         /**
202          * Gets the message sender this rule matches
203          *
204          * NOTE: Use HighlightRule::senderMatcher() for performing matches
205          *
206          * @return String representing a phrase or expression to match
207          */
208         inline QString sender() const { return _sender; }
209         /**
210          * Sets the message sender this rule matches
211          *
212          * @param sender String representing a phrase or expression to match
213          */
214         inline void setSender(const QString &sender) {
215             _sender = sender;
216             _cacheInvalid = true;
217         }
218
219         /**
220          * Gets the channel name this rule matches
221          *
222          * NOTE: Use HighlightRule::chanNameMatcher() for performing matches
223          *
224          * @return String representing a phrase or expression to match
225          */
226         inline QString chanName() const { return _chanName; }
227         /**
228          * Sets the channel name this rule matches
229          *
230          * @param chanName String representing a phrase or expression to match
231          */
232         inline void setChanName(const QString &chanName) {
233             _chanName = chanName;
234             _cacheInvalid = true;
235         }
236
237         /**
238          * Gets the expression matcher for the message contents, caching if needed
239          *
240          * @return Expression matcher to compare with message contents
241          */
242         inline ExpressionMatch contentsMatcher() const {
243             if (_cacheInvalid) {
244                 determineExpressions();
245             }
246             return _contentsMatch;
247         }
248
249         /**
250          * Gets the expression matcher for the message sender, caching if needed
251          *
252          * @return Expression matcher to compare with message sender
253          */
254         inline ExpressionMatch senderMatcher() const {
255             if (_cacheInvalid) {
256                 determineExpressions();
257             }
258             return _senderMatch;
259         }
260
261         /**
262          * Gets the expression matcher for the channel name, caching if needed
263          *
264          * @return Expression matcher to compare with channel name
265          */
266         inline ExpressionMatch chanNameMatcher() const {
267             if (_cacheInvalid) {
268                 determineExpressions();
269             }
270             return _chanNameMatch;
271         }
272
273         bool operator!=(const HighlightRule &other) const;
274
275     private:
276         /**
277          * Update internal cache of expression matching if needed
278          */
279         void determineExpressions() const;
280
281         int _id = -1;
282         QString _contents = {};
283         bool _isRegEx = false;
284         bool _isCaseSensitive = false;
285         bool _isEnabled = true;
286         bool _isInverse = false;
287         QString _sender = {};
288         QString _chanName = {};
289
290         // These represent internal cache and should be safe to mutate in 'const' functions
291         // See https://stackoverflow.com/questions/3141087/what-is-meant-with-const-at-end-of-function-declaration
292         mutable bool _cacheInvalid = true;           ///< If true, match cache needs redone
293         mutable ExpressionMatch _contentsMatch = {}; ///< Expression match cache for message content
294         mutable ExpressionMatch _senderMatch = {};   ///< Expression match cache for sender
295         mutable ExpressionMatch _chanNameMatch = {}; ///< Expression match cache for channel name
296     };
297
298     using HighlightRuleList = QList<HighlightRule>;
299
300     int indexOf(int rule) const;
301     inline bool contains(int rule) const { return indexOf(rule) != -1; }
302     inline bool isEmpty() const { return _highlightRuleList.isEmpty(); }
303     inline int count() const { return _highlightRuleList.count(); }
304     inline void removeAt(int index) { _highlightRuleList.removeAt(index); }
305     inline void clear() { _highlightRuleList.clear(); }
306     inline HighlightRule &operator[](int i) { return _highlightRuleList[i]; }
307     inline const HighlightRule &operator[](int i) const { return _highlightRuleList.at(i); }
308     inline const HighlightRuleList &highlightRuleList() const { return _highlightRuleList; }
309
310     int nextId();
311
312     inline int highlightNick() { return _highlightNick; }
313     inline bool nicksCaseSensitive() { return _nicksCaseSensitive; }
314
315     //! Check if a message matches the HighlightRule
316     /** This method checks if a message matches the users highlight rules.
317       * \param msg The Message that should be checked
318       */
319     bool match(const Message &msg, const QString &currentNick, const QStringList &identityNicks);
320
321 public slots:
322     virtual QVariantMap initHighlightRuleList() const;
323     virtual void initSetHighlightRuleList(const QVariantMap &HighlightRuleList);
324
325     //! Request removal of an ignore rule based on the rule itself.
326     /** Use this method if you want to remove a single ignore rule
327       * and get that synced with the core immediately.
328       * \param highlightRule A valid ignore rule
329       */
330     virtual inline void requestRemoveHighlightRule(int highlightRule) { REQUEST(ARG(highlightRule)) }
331     virtual void removeHighlightRule(int highlightRule);
332
333     //! Request toggling of "isEnabled" flag of a given ignore rule.
334     /** Use this method if you want to toggle the "isEnabled" flag of a single ignore rule
335       * and get that synced with the core immediately.
336       * \param highlightRule A valid ignore rule
337       */
338     virtual inline void requestToggleHighlightRule(int highlightRule) { REQUEST(ARG(highlightRule)) }
339     virtual void toggleHighlightRule(int highlightRule);
340
341     //! Request an HighlightRule to be added to the ignore list
342     /** Items added to the list with this method, get immediately synced with the core
343       * \param name The rule
344       * \param isRegEx If the rule should be interpreted as a nickname, or a regex
345       * \param isCaseSensitive If the rule should be interpreted as case-sensitive
346       * \param isEnabled If the rule is active
347       * @param chanName The channel in which the rule should apply
348       */
349     virtual inline void requestAddHighlightRule(int id, const QString &name, bool isRegEx, bool isCaseSensitive, bool isEnabled,
350                                                 bool isInverse, const QString &sender, const QString &chanName)
351     {
352         REQUEST(ARG(id), ARG(name), ARG(isRegEx), ARG(isCaseSensitive), ARG(isEnabled), ARG(isInverse), ARG(sender),
353                 ARG(chanName))
354     }
355
356
357     virtual void addHighlightRule(int id, const QString &name, bool isRegEx, bool isCaseSensitive, bool isEnabled,
358                                   bool isInverse, const QString &sender, const QString &chanName);
359
360     virtual inline void requestSetHighlightNick(int highlightNick)
361     {
362         REQUEST(ARG(highlightNick))
363     }
364
365     inline void setHighlightNick(int highlightNick) {
366         _highlightNick = static_cast<HighlightNickType>(highlightNick);
367         _cacheNickConfigInvalid = true;
368     }
369
370     virtual inline void requestSetNicksCaseSensitive(bool nicksCaseSensitive)
371     {
372         REQUEST(ARG(nicksCaseSensitive))
373     }
374
375     inline void setNicksCaseSensitive(bool nicksCaseSensitive) {
376         _nicksCaseSensitive = nicksCaseSensitive;
377         _cacheNickConfigInvalid = true;
378     }
379
380 protected:
381     void setHighlightRuleList(const QList<HighlightRule> &HighlightRuleList) { _highlightRuleList = HighlightRuleList; }
382
383     bool match(const QString &msgContents,
384                const QString &msgSender,
385                Message::Type msgType,
386                Message::Flags msgFlags,
387                const QString &bufferName,
388                const QString &currentNick,
389                const QStringList identityNicks);
390
391 signals:
392     void ruleAdded(QString name, bool isRegEx, bool isCaseSensitive, bool isEnabled, bool isInverse, QString sender, QString chanName);
393
394 private:
395     /**
396      * Update internal cache of expression matching if needed
397      */
398     void determineNickExpressions(const QString &currentNick,
399                                   const QStringList identityNicks) const;
400
401     /**
402      * Check if nickname matching cache is invalid
403      * @param currentNick
404      * @param identityNicks
405      * @return
406      */
407     bool cacheNickInvalid(const QString &currentNick, const QStringList identityNicks) const {
408         if (_cacheNickConfigInvalid) return true;
409         if (_cachedNickCurrent != currentNick) return true;
410         if (_cachedIdentityNicks != identityNicks) return true;
411     }
412
413     HighlightRuleList _highlightRuleList;
414     HighlightNickType _highlightNick = HighlightNickType::CurrentNick;
415     bool _nicksCaseSensitive = false;
416
417     // These represent internal cache and should be safe to mutate in 'const' functions
418     mutable bool _cacheNickConfigInvalid = true;     ///< If true, nick match cache needs redone
419     mutable QString _cachedNickCurrent = {};         ///< Last cached current nick
420     mutable QStringList _cachedIdentityNicks = {};   ///< Last cached identity nicks
421     mutable ExpressionMatch _cachedNickMatcher = {}; ///< Expression match cache for nicks
422 };