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 #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
QtUiMessageProcessor(QObject * parent)29 QtUiMessageProcessor::QtUiMessageProcessor(QObject* parent)
30 : AbstractMessageProcessor(parent)
31 , _processing(false)
32 , _processMode(TimerBased)
33 {
34 NotificationSettings notificationSettings;
35 _nicksCaseSensitive = notificationSettings.nicksCaseSensitive();
36 _nickMatcher.setCaseSensitive(_nicksCaseSensitive);
37 _highlightNick = notificationSettings.highlightNick();
38 _nickMatcher.setHighlightMode(static_cast<NickHighlightMatcher::HighlightNickType>(_highlightNick));
39 highlightListChanged(notificationSettings.highlightList());
40 notificationSettings.notify("Highlights/NicksCaseSensitive", this, &QtUiMessageProcessor::nicksCaseSensitiveChanged);
41 notificationSettings.notify("Highlights/CustomList", this, &QtUiMessageProcessor::highlightListChanged);
42 notificationSettings.notify("Highlights/HighlightNick", this, &QtUiMessageProcessor::highlightNickChanged);
43
44 _processTimer.setInterval(0);
45 connect(&_processTimer, &QTimer::timeout, this, &QtUiMessageProcessor::processNextMessage);
46 }
47
reset()48 void QtUiMessageProcessor::reset()
49 {
50 if (processMode() == TimerBased) {
51 if (_processTimer.isActive())
52 _processTimer.stop();
53 _processing = false;
54 _currentBatch.clear();
55 _processQueue.clear();
56 }
57 }
58
process(Message & msg)59 void QtUiMessageProcessor::process(Message& msg)
60 {
61 checkForHighlight(msg);
62 preProcess(msg);
63 Client::messageModel()->insertMessage(msg);
64 }
65
process(QList<Message> & msgs)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())
79 return;
80 _processQueue.append(msgs);
81 if (!isProcessing())
82 startProcessing();
83 }
84
startProcessing()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
processNextMessage()96 void QtUiMessageProcessor::processNextMessage()
97 {
98 if (_currentBatch.isEmpty()) {
99 if (_processQueue.isEmpty()) {
100 _processTimer.stop();
101 _processing = false;
102 return;
103 }
104 _currentBatch = _processQueue.takeFirst();
105 }
106 Message msg = _currentBatch.takeFirst();
107 process(msg);
108 }
109
checkForHighlight(Message & msg)110 void QtUiMessageProcessor::checkForHighlight(Message& msg)
111 {
112 if (!((msg.type() & (Message::Plain | Message::Notice | Message::Action)) && !(msg.flags() & Message::Self)))
113 return;
114
115 // Cached per network
116 const NetworkId& netId = msg.bufferInfo().networkId();
117 const Network* net = Client::network(netId);
118
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 // Nickname matching allowed and current nickname is known
170 // Run the nickname matcher on the unformatted string
171 if (_nickMatcher.match(stripFormatCodes(msgContents), netId, currentNick, identityNicks)) {
172 msg.setFlags(msg.flags() | Message::Highlight);
173 return;
174 }
175 }
176 }
177 }
178
nicksCaseSensitiveChanged(const QVariant & variant)179 void QtUiMessageProcessor::nicksCaseSensitiveChanged(const QVariant& variant)
180 {
181 _nicksCaseSensitive = variant.toBool();
182 // Update nickname matcher, too
183 _nickMatcher.setCaseSensitive(_nicksCaseSensitive);
184 }
185
highlightListChanged(const QVariant & variant)186 void QtUiMessageProcessor::highlightListChanged(const QVariant& variant)
187 {
188 QVariantList varList = variant.toList();
189
190 _highlightRuleList.clear();
191 QVariantList::const_iterator iter = varList.constBegin();
192 while (iter != varList.constEnd()) {
193 QVariantMap rule = iter->toMap();
194 _highlightRuleList << LegacyHighlightRule(rule["Name"].toString(),
195 rule["RegEx"].toBool(),
196 rule["CS"].toBool(),
197 rule["Enable"].toBool(),
198 rule["Channel"].toString());
199 ++iter;
200 }
201 }
202
highlightNickChanged(const QVariant & variant)203 void QtUiMessageProcessor::highlightNickChanged(const QVariant& variant)
204 {
205 _highlightNick = (HighlightNickType)variant.toInt();
206 // Convert from QtUiMessageProcessor::HighlightNickType (which is from NotificationSettings) to
207 // NickHighlightMatcher::HighlightNickType
208 _nickMatcher.setHighlightMode(static_cast<NickHighlightMatcher::HighlightNickType>(_highlightNick));
209 }
210
networkRemoved(NetworkId id)211 void QtUiMessageProcessor::networkRemoved(NetworkId id)
212 {
213 // Clean up nickname matching cache
214 _nickMatcher.removeNetwork(id);
215 }
216
217 /**************************************************************************
218 * LegacyHighlightRule
219 *************************************************************************/
operator !=(const LegacyHighlightRule & other) const220 bool QtUiMessageProcessor::LegacyHighlightRule::operator!=(const LegacyHighlightRule& other) const
221 {
222 return (_contents != other._contents || _isRegEx != other._isRegEx || _isCaseSensitive != other._isCaseSensitive
223 || _isEnabled != other._isEnabled || _chanName != other._chanName);
224 // Don't compare ExpressionMatch objects as they are created as needed from the above
225 }
226
determineExpressions() const227 void QtUiMessageProcessor::LegacyHighlightRule::determineExpressions() const
228 {
229 // Don't update if not needed
230 if (!_cacheInvalid) {
231 return;
232 }
233
234 // Set up matching rules
235 // Message is either phrase or regex
236 ExpressionMatch::MatchMode contentsMode = _isRegEx ? ExpressionMatch::MatchMode::MatchRegEx : ExpressionMatch::MatchMode::MatchPhrase;
237 // Sender (when added) and channel are either multiple wildcard entries or regex
238 ExpressionMatch::MatchMode scopeMode = _isRegEx ? ExpressionMatch::MatchMode::MatchRegEx : ExpressionMatch::MatchMode::MatchMultiWildcard;
239
240 _contentsMatch = ExpressionMatch(_contents, contentsMode, _isCaseSensitive);
241 _chanNameMatch = ExpressionMatch(_chanName, scopeMode, _isCaseSensitive);
242
243 _cacheInvalid = false;
244 }
245