client: Port old HighlightRule to ExpressionMatch
[quassel.git] / src / qtui / qtuimessageprocessor.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 "qtuimessageprocessor.h"
22
23 #include "client.h"
24 #include "clientsettings.h"
25 #include "identity.h"
26 #include "messagemodel.h"
27 #include "network.h"
28
29 QtUiMessageProcessor::QtUiMessageProcessor(QObject *parent)
30     : AbstractMessageProcessor(parent),
31     _processing(false),
32     _processMode(TimerBased)
33 {
34     NotificationSettings notificationSettings;
35     _nicksCaseSensitive = notificationSettings.nicksCaseSensitive();
36     _highlightNick = notificationSettings.highlightNick();
37     highlightListChanged(notificationSettings.highlightList());
38     notificationSettings.notify("Highlights/NicksCaseSensitive", this, SLOT(nicksCaseSensitiveChanged(const QVariant &)));
39     notificationSettings.notify("Highlights/CustomList", this, SLOT(highlightListChanged(const QVariant &)));
40     notificationSettings.notify("Highlights/HighlightNick", this, SLOT(highlightNickChanged(const QVariant &)));
41
42     _processTimer.setInterval(0);
43     connect(&_processTimer, SIGNAL(timeout()), this, SLOT(processNextMessage()));
44 }
45
46
47 void QtUiMessageProcessor::reset()
48 {
49     if (processMode() == TimerBased) {
50         if (_processTimer.isActive()) _processTimer.stop();
51         _processing = false;
52         _currentBatch.clear();
53         _processQueue.clear();
54     }
55 }
56
57
58 void QtUiMessageProcessor::process(Message &msg)
59 {
60     checkForHighlight(msg);
61     preProcess(msg);
62     Client::messageModel()->insertMessage(msg);
63 }
64
65
66 void QtUiMessageProcessor::process(QList<Message> &msgs)
67 {
68     QList<Message>::iterator msgIter = msgs.begin();
69     QList<Message>::iterator msgIterEnd = msgs.end();
70     while (msgIter != msgIterEnd) {
71         checkForHighlight(*msgIter);
72         preProcess(*msgIter);
73         ++msgIter;
74     }
75     Client::messageModel()->insertMessages(msgs);
76     return;
77
78     if (msgs.isEmpty()) return;
79     _processQueue.append(msgs);
80     if (!isProcessing())
81         startProcessing();
82 }
83
84
85 void QtUiMessageProcessor::startProcessing()
86 {
87     if (processMode() == TimerBased) {
88         if (_currentBatch.isEmpty() && _processQueue.isEmpty())
89             return;
90         _processing = true;
91         if (!_processTimer.isActive())
92             _processTimer.start();
93     }
94 }
95
96
97 void QtUiMessageProcessor::processNextMessage()
98 {
99     if (_currentBatch.isEmpty()) {
100         if (_processQueue.isEmpty()) {
101             _processTimer.stop();
102             _processing = false;
103             return;
104         }
105         _currentBatch = _processQueue.takeFirst();
106     }
107     Message msg = _currentBatch.takeFirst();
108     process(msg);
109 }
110
111
112 void QtUiMessageProcessor::checkForHighlight(Message &msg)
113 {
114     if (!((msg.type() & (Message::Plain | Message::Notice | Message::Action)) && !(msg.flags() & Message::Self)))
115         return;
116
117     // TODO: Cache this (per network)
118     const Network *net = Client::network(msg.bufferInfo().networkId());
119     if (net && !net->myNick().isEmpty()) {
120         // Get current nick
121         QString currentNick = net->myNick();
122         // Get identity nicks
123         QStringList identityNicks = {};
124         const Identity *myIdentity = Client::identity(net->identity());
125         if (myIdentity) {
126             identityNicks = myIdentity->nicks();
127         }
128
129         // Get buffer name, message contents
130         QString bufferName = msg.bufferInfo().bufferName();
131         QString msgContents = msg.contents();
132         bool matches = false;
133
134         for (int i = 0; i < _highlightRuleList.count(); i++) {
135             auto &rule = _highlightRuleList.at(i);
136             if (!rule.isEnabled())
137                 continue;
138
139             // Skip if channel name doesn't match and channel rule is not empty
140             //
141             // Match succeeds if...
142             //   Channel name matches a defined rule
143             //   Defined rule is empty
144             // And take the inverse of the above
145             if (!rule.chanNameMatcher().match(bufferName, true)) {
146                 // A channel name rule is specified and does NOT match the current buffer name, skip
147                 // this rule
148                 continue;
149             }
150
151             // Check message according to specified rule, allowing empty rules to match
152             bool contentsMatch = rule.contentsMatcher().match(stripFormatCodes(msgContents), true);
153
154             // Support for sender matching can be added here
155
156             if (contentsMatch) {
157                 // Support for inverse rules can be added here
158                 matches = true;
159             }
160         }
161
162         if (matches) {
163             msg.setFlags(msg.flags() | Message::Highlight);
164             return;
165         }
166
167         // Check nicknames
168         if (_highlightNick != HighlightNickType::NoNick && !currentNick.isEmpty()) {
169             // Update cache if needed
170             determineNickExpressions(currentNick, identityNicks);
171
172             // Check for a match
173             if (_cachedNickMatcher.isValid()
174                     && _cachedNickMatcher.match(stripFormatCodes(msgContents))) {
175                 // Nick matcher is valid and match found
176                 msg.setFlags(msg.flags() | Message::Highlight);
177                 return;
178             }
179         }
180     }
181 }
182
183
184 void QtUiMessageProcessor::nicksCaseSensitiveChanged(const QVariant &variant)
185 {
186     _nicksCaseSensitive = variant.toBool();
187     _cacheNickConfigInvalid = true;
188 }
189
190
191 void QtUiMessageProcessor::highlightListChanged(const QVariant &variant)
192 {
193     QVariantList varList = variant.toList();
194
195     _highlightRuleList.clear();
196     QVariantList::const_iterator iter = varList.constBegin();
197     while (iter != varList.constEnd()) {
198         QVariantMap rule = iter->toMap();
199         _highlightRuleList << LegacyHighlightRule(rule["Name"].toString(),
200                 rule["RegEx"].toBool(),
201                 rule["CS"].toBool(),
202                 rule["Enable"].toBool(),
203                 rule["Channel"].toString());
204         ++iter;
205     }
206 }
207
208
209 void QtUiMessageProcessor::highlightNickChanged(const QVariant &variant)
210 {
211     _highlightNick = (NotificationSettings::HighlightNickType)variant.toInt();
212     _cacheNickConfigInvalid = true;
213 }
214
215
216 void QtUiMessageProcessor::determineNickExpressions(const QString &currentNick,
217                                                     const QStringList identityNicks) const
218 {
219     // Don't do anything for no nicknames
220     if (_highlightNick == HighlightNickType::NoNick) {
221         return;
222     }
223
224     // Only update if needed (check nickname config, current nick, identity nicks for change)
225     if (!_cacheNickConfigInvalid
226           && _cachedNickCurrent == currentNick
227           && _cachedIdentityNicks == identityNicks) {
228         return;
229     }
230
231     // Add all nicknames
232     QStringList nickList;
233     if (_highlightNick == HighlightNickType::CurrentNick) {
234         nickList << currentNick;
235     }
236     else if (_highlightNick == HighlightNickType::AllNicks) {
237         nickList = identityNicks;
238         if (!nickList.contains(currentNick))
239             nickList.prepend(currentNick);
240     }
241
242     // Set up phrase matcher, joining with newlines
243     _cachedNickMatcher = ExpressionMatch(nickList.join("\n"),
244                                         ExpressionMatch::MatchMode::MatchMultiPhrase,
245                                         _nicksCaseSensitive);
246
247     _cacheNickConfigInvalid = false;
248     _cachedNickCurrent = currentNick;
249     _cachedIdentityNicks = identityNicks;
250 }
251
252
253 /**************************************************************************
254  * LegacyHighlightRule
255  *************************************************************************/
256 bool QtUiMessageProcessor::LegacyHighlightRule::operator!=(const LegacyHighlightRule &other) const
257 {
258     return (_contents != other._contents ||
259             _isRegEx != other._isRegEx ||
260             _isCaseSensitive != other._isCaseSensitive ||
261             _isEnabled != other._isEnabled ||
262             _chanName != other._chanName);
263     // Don't compare ExpressionMatch objects as they are created as needed from the above
264 }
265
266
267 void QtUiMessageProcessor::LegacyHighlightRule::determineExpressions() const
268 {
269     // Don't update if not needed
270     if (!_cacheInvalid) {
271         return;
272     }
273
274     // Set up matching rules
275     // Message is either phrase or regex
276     ExpressionMatch::MatchMode contentsMode =
277             _isRegEx ? ExpressionMatch::MatchMode::MatchRegEx :
278                        ExpressionMatch::MatchMode::MatchPhrase;
279     // Sender (when added) and channel are either multiple wildcard entries or regex
280     ExpressionMatch::MatchMode scopeMode =
281             _isRegEx ? ExpressionMatch::MatchMode::MatchRegEx :
282                        ExpressionMatch::MatchMode::MatchMultiWildcard;
283
284     _contentsMatch = ExpressionMatch(_contents, contentsMode, _isCaseSensitive);
285     _chanNameMatch = ExpressionMatch(_chanName, scopeMode, _isCaseSensitive);
286
287     _cacheInvalid = false;
288 }