common: Port HighlightRule to ExpressionMatch
[quassel.git] / src / common / highlightrulemanager.cpp
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 #include "highlightrulemanager.h"
22
23 #include <QDebug>
24
25 #include "expressionmatch.h"
26 #include "util.h"
27
28 INIT_SYNCABLE_OBJECT(HighlightRuleManager)
29
30 HighlightRuleManager &HighlightRuleManager::operator=(const HighlightRuleManager &other)
31 {
32     if (this == &other)
33         return *this;
34
35     SyncableObject::operator=(other);
36     _highlightRuleList = other._highlightRuleList;
37     _nicksCaseSensitive = other._nicksCaseSensitive;
38     _highlightNick = other._highlightNick;
39     return *this;
40 }
41
42
43 int HighlightRuleManager::indexOf(int id) const
44 {
45     for (int i = 0; i < _highlightRuleList.count(); i++) {
46         if (_highlightRuleList[i].id() == id)
47             return i;
48     }
49     return -1;
50 }
51
52
53 int HighlightRuleManager::nextId()
54 {
55     int max = 0;
56     for (int i = 0; i < _highlightRuleList.count(); i++) {
57         int id = _highlightRuleList[i].id();
58         if (id > max) {
59             max = id;
60         }
61     }
62     return max + 1;
63 }
64
65
66 QVariantMap HighlightRuleManager::initHighlightRuleList() const
67 {
68     QVariantList id;
69     QVariantMap highlightRuleListMap;
70     QStringList name;
71     QVariantList isRegEx;
72     QVariantList isCaseSensitive;
73     QVariantList isActive;
74     QVariantList isInverse;
75     QStringList sender;
76     QStringList channel;
77
78     for (int i = 0; i < _highlightRuleList.count(); i++) {
79         id << _highlightRuleList[i].id();
80         name << _highlightRuleList[i].contents();
81         isRegEx << _highlightRuleList[i].isRegEx();
82         isCaseSensitive << _highlightRuleList[i].isCaseSensitive();
83         isActive << _highlightRuleList[i].isEnabled();
84         isInverse << _highlightRuleList[i].isInverse();
85         sender << _highlightRuleList[i].sender();
86         channel << _highlightRuleList[i].chanName();
87     }
88
89     highlightRuleListMap["id"] = id;
90     highlightRuleListMap["name"] = name;
91     highlightRuleListMap["isRegEx"] = isRegEx;
92     highlightRuleListMap["isCaseSensitive"] = isCaseSensitive;
93     highlightRuleListMap["isEnabled"] = isActive;
94     highlightRuleListMap["isInverse"] = isInverse;
95     highlightRuleListMap["sender"] = sender;
96     highlightRuleListMap["channel"] = channel;
97     return highlightRuleListMap;
98 }
99
100
101 void HighlightRuleManager::initSetHighlightRuleList(const QVariantMap &highlightRuleList)
102 {
103     QVariantList id = highlightRuleList["id"].toList();
104     QStringList name = highlightRuleList["name"].toStringList();
105     QVariantList isRegEx = highlightRuleList["isRegEx"].toList();
106     QVariantList isCaseSensitive = highlightRuleList["isCaseSensitive"].toList();
107     QVariantList isActive = highlightRuleList["isEnabled"].toList();
108     QVariantList isInverse = highlightRuleList["isInverse"].toList();
109     QStringList sender = highlightRuleList["sender"].toStringList();
110     QStringList channel = highlightRuleList["channel"].toStringList();
111
112     int count = id.count();
113     if (count != name.count() || count != isRegEx.count() || count != isCaseSensitive.count() ||
114         count != isActive.count() || count != isInverse.count() || count != sender.count() ||
115         count != channel.count()) {
116         qWarning() << "Corrupted HighlightRuleList settings! (Count mismatch)";
117         return;
118     }
119
120     _highlightRuleList.clear();
121     for (int i = 0; i < name.count(); i++) {
122         _highlightRuleList << HighlightRule(id[i].toInt(), name[i], isRegEx[i].toBool(), isCaseSensitive[i].toBool(),
123                                             isActive[i].toBool(), isInverse[i].toBool(), sender[i], channel[i]);
124     }
125 }
126
127
128 void HighlightRuleManager::addHighlightRule(int id, const QString &name, bool isRegEx, bool isCaseSensitive,
129                                             bool isActive, bool isInverse, const QString &sender,
130                                             const QString &channel)
131 {
132     if (contains(id)) {
133         return;
134     }
135
136     HighlightRule newItem = HighlightRule(id, name, isRegEx, isCaseSensitive, isActive, isInverse, sender, channel);
137     _highlightRuleList << newItem;
138
139     SYNC(ARG(id), ARG(name), ARG(isRegEx), ARG(isCaseSensitive), ARG(isActive), ARG(isInverse), ARG(sender),
140          ARG(channel))
141 }
142
143
144 bool HighlightRuleManager::match(const QString &msgContents,
145                                  const QString &msgSender,
146                                  Message::Type msgType,
147                                  Message::Flags msgFlags,
148                                  const QString &bufferName,
149                                  const QString &currentNick,
150                                  const QStringList identityNicks)
151 {
152     if (!((msgType & (Message::Plain | Message::Notice | Message::Action)) && !(msgFlags & Message::Self))) {
153        return false;
154     }
155
156     bool matches = false;
157
158     for (int i = 0; i < _highlightRuleList.count(); i++) {
159         auto &rule = _highlightRuleList.at(i);
160         if (!rule.isEnabled())
161             continue;
162
163         // Skip if channel name doesn't match and channel rule is not empty
164         //
165         // Match succeeds if...
166         //   Channel name matches a defined rule
167         //   Defined rule is empty
168         // And take the inverse of the above
169         if (!rule.chanNameMatcher().match(bufferName, true)) {
170             // A channel name rule is specified and does NOT match the current buffer name, skip
171             // this rule
172             continue;
173         }
174
175         // Check message according to specified rule, allowing empty rules to match
176         bool contentsMatch = rule.contentsMatcher().match(stripFormatCodes(msgContents), true);
177
178         // Check sender according to specified rule, allowing empty rules to match
179         bool senderMatch = rule.senderMatcher().match(msgSender, true);
180
181         if (contentsMatch && senderMatch) {
182             // If an inverse rule matches, then we know that we never want to return a highlight.
183             if (rule.isInverse()) {
184                 return false;
185             }
186             else {
187                 matches = true;
188             }
189         }
190     }
191
192     if (matches)
193         return true;
194
195     // Check nicknames
196     if (_highlightNick != HighlightNickType::NoNick && !currentNick.isEmpty()) {
197         // Update cache if needed
198         determineNickExpressions(currentNick, identityNicks);
199
200         // Check for a match
201         if (_cachedNickMatcher.isValid()
202                 && _cachedNickMatcher.match(stripFormatCodes(msgContents))) {
203             // Nick matcher is valid and match found
204             return true;
205         }
206     }
207
208     return false;
209 }
210
211
212 void HighlightRuleManager::removeHighlightRule(int highlightRule)
213 {
214     removeAt(indexOf(highlightRule));
215     SYNC(ARG(highlightRule))
216 }
217
218
219 void HighlightRuleManager::toggleHighlightRule(int highlightRule)
220 {
221     int idx = indexOf(highlightRule);
222     if (idx == -1)
223         return;
224     _highlightRuleList[idx].setIsEnabled(!_highlightRuleList[idx].isEnabled());
225     SYNC(ARG(highlightRule))
226 }
227
228
229 bool HighlightRuleManager::match(const Message &msg, const QString &currentNick, const QStringList &identityNicks)
230 {
231     return match(msg.contents(), msg.sender(), msg.type(), msg.flags(), msg.bufferInfo().bufferName(), currentNick, identityNicks);
232 }
233
234
235 void HighlightRuleManager::determineNickExpressions(const QString &currentNick,
236                                                     const QStringList identityNicks) const
237 {
238     // Don't do anything for no nicknames
239     if (_highlightNick == HighlightNickType::NoNick) {
240         return;
241     }
242
243     // Only update if needed (check nickname config, current nick, identity nicks for change)
244     if (!_cacheNickConfigInvalid
245           && _cachedNickCurrent == currentNick
246           && _cachedIdentityNicks == identityNicks) {
247         return;
248     }
249
250     // Add all nicknames
251     QStringList nickList;
252     if (_highlightNick == CurrentNick) {
253         nickList << currentNick;
254     }
255     else if (_highlightNick == AllNicks) {
256         nickList = identityNicks;
257         if (!nickList.contains(currentNick))
258             nickList.prepend(currentNick);
259     }
260
261     // Set up phrase matcher, joining with newlines
262     _cachedNickMatcher = ExpressionMatch(nickList.join("\n"),
263                                         ExpressionMatch::MatchMode::MatchMultiPhrase,
264                                         _nicksCaseSensitive);
265
266     _cacheNickConfigInvalid = false;
267     _cachedNickCurrent = currentNick;
268     _cachedIdentityNicks = identityNicks;
269 }
270
271
272 /**************************************************************************
273  * HighlightRule
274  *************************************************************************/
275 bool HighlightRuleManager::HighlightRule::operator!=(const HighlightRule &other) const
276 {
277     return (_id != other._id ||
278             _contents != other._contents ||
279             _isRegEx != other._isRegEx ||
280             _isCaseSensitive != other._isCaseSensitive ||
281             _isEnabled != other._isEnabled ||
282             _isInverse != other._isInverse ||
283             _sender != other._sender ||
284             _chanName != other._chanName);
285     // Don't compare ExpressionMatch objects as they are created as needed from the above
286 }
287
288
289 void HighlightRuleManager::HighlightRule::determineExpressions() const
290 {
291     // Don't update if not needed
292     if (!_cacheInvalid) {
293         return;
294     }
295
296     // Set up matching rules
297     // Message is either phrase or regex
298     ExpressionMatch::MatchMode contentsMode =
299             _isRegEx ? ExpressionMatch::MatchMode::MatchRegEx :
300                        ExpressionMatch::MatchMode::MatchPhrase;
301     // Sender and channel are either multiple wildcard entries or regex
302     ExpressionMatch::MatchMode scopeMode =
303             _isRegEx ? ExpressionMatch::MatchMode::MatchRegEx :
304                        ExpressionMatch::MatchMode::MatchMultiWildcard;
305
306     _contentsMatch = ExpressionMatch(_contents, contentsMode, _isCaseSensitive);
307     _senderMatch = ExpressionMatch(_sender, scopeMode, _isCaseSensitive);
308     _chanNameMatch = ExpressionMatch(_chanName, scopeMode, _isCaseSensitive);
309
310     _cacheInvalid = false;
311 }