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