Let RemotePeer(s) tell their protocol type and features
[quassel.git] / src / core / ctcpparser.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2014 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 "ctcpparser.h"
22
23 #include "corenetworkconfig.h"
24 #include "coresession.h"
25 #include "ctcpevent.h"
26 #include "messageevent.h"
27
28 const QByteArray XDELIM = "\001";
29
30 CtcpParser::CtcpParser(CoreSession *coreSession, QObject *parent)
31     : QObject(parent),
32     _coreSession(coreSession)
33 {
34     QByteArray MQUOTE = QByteArray("\020");
35     _ctcpMDequoteHash[MQUOTE + '0'] = QByteArray(1, '\000');
36     _ctcpMDequoteHash[MQUOTE + 'n'] = QByteArray(1, '\n');
37     _ctcpMDequoteHash[MQUOTE + 'r'] = QByteArray(1, '\r');
38     _ctcpMDequoteHash[MQUOTE + MQUOTE] = MQUOTE;
39
40     setStandardCtcp(_coreSession->networkConfig()->standardCtcp());
41
42     connect(_coreSession->networkConfig(), SIGNAL(standardCtcpSet(bool)), this, SLOT(setStandardCtcp(bool)));
43     connect(this, SIGNAL(newEvent(Event *)), _coreSession->eventManager(), SLOT(postEvent(Event *)));
44 }
45
46
47 void CtcpParser::setStandardCtcp(bool enabled)
48 {
49     QByteArray XQUOTE = QByteArray("\134");
50     if (enabled)
51         _ctcpXDelimDequoteHash[XQUOTE + XQUOTE] = XQUOTE;
52     else
53         _ctcpXDelimDequoteHash.remove(XQUOTE + XQUOTE);
54     _ctcpXDelimDequoteHash[XQUOTE + QByteArray("a")] = XDELIM;
55 }
56
57
58 void CtcpParser::displayMsg(NetworkEvent *event, Message::Type msgType, const QString &msg, const QString &sender,
59     const QString &target, Message::Flags msgFlags)
60 {
61     if (event->testFlag(EventManager::Silent))
62         return;
63
64     MessageEvent *msgEvent = new MessageEvent(msgType, event->network(), msg, sender, target, msgFlags);
65     msgEvent->setTimestamp(event->timestamp());
66
67     emit newEvent(msgEvent);
68 }
69
70
71 QByteArray CtcpParser::lowLevelQuote(const QByteArray &message)
72 {
73     QByteArray quotedMessage = message;
74
75     QHash<QByteArray, QByteArray> quoteHash = _ctcpMDequoteHash;
76     QByteArray MQUOTE = QByteArray("\020");
77     quoteHash.remove(MQUOTE + MQUOTE);
78     quotedMessage.replace(MQUOTE, MQUOTE + MQUOTE);
79
80     QHash<QByteArray, QByteArray>::const_iterator quoteIter = quoteHash.constBegin();
81     while (quoteIter != quoteHash.constEnd()) {
82         quotedMessage.replace(quoteIter.value(), quoteIter.key());
83         quoteIter++;
84     }
85     return quotedMessage;
86 }
87
88
89 QByteArray CtcpParser::lowLevelDequote(const QByteArray &message)
90 {
91     QByteArray dequotedMessage;
92     QByteArray messagepart;
93     QHash<QByteArray, QByteArray>::iterator ctcpquote;
94
95     // copy dequote Message
96     for (int i = 0; i < message.size(); i++) {
97         messagepart = message.mid(i, 1);
98         if (i+1 < message.size()) {
99             for (ctcpquote = _ctcpMDequoteHash.begin(); ctcpquote != _ctcpMDequoteHash.end(); ++ctcpquote) {
100                 if (message.mid(i, 2) == ctcpquote.key()) {
101                     messagepart = ctcpquote.value();
102                     ++i;
103                     break;
104                 }
105             }
106         }
107         dequotedMessage += messagepart;
108     }
109     return dequotedMessage;
110 }
111
112
113 QByteArray CtcpParser::xdelimQuote(const QByteArray &message)
114 {
115     QByteArray quotedMessage = message;
116     QHash<QByteArray, QByteArray>::const_iterator quoteIter = _ctcpXDelimDequoteHash.constBegin();
117     while (quoteIter != _ctcpXDelimDequoteHash.constEnd()) {
118         quotedMessage.replace(quoteIter.value(), quoteIter.key());
119         quoteIter++;
120     }
121     return quotedMessage;
122 }
123
124
125 QByteArray CtcpParser::xdelimDequote(const QByteArray &message)
126 {
127     QByteArray dequotedMessage;
128     QByteArray messagepart;
129     QHash<QByteArray, QByteArray>::iterator xdelimquote;
130
131     for (int i = 0; i < message.size(); i++) {
132         messagepart = message.mid(i, 1);
133         if (i+1 < message.size()) {
134             for (xdelimquote = _ctcpXDelimDequoteHash.begin(); xdelimquote != _ctcpXDelimDequoteHash.end(); ++xdelimquote) {
135                 if (message.mid(i, 2) == xdelimquote.key()) {
136                     messagepart = xdelimquote.value();
137                     i++;
138                     break;
139                 }
140             }
141         }
142         dequotedMessage += messagepart;
143     }
144     return dequotedMessage;
145 }
146
147
148 void CtcpParser::processIrcEventRawNotice(IrcEventRawMessage *event)
149 {
150     parse(event, Message::Notice);
151 }
152
153
154 void CtcpParser::processIrcEventRawPrivmsg(IrcEventRawMessage *event)
155 {
156     parse(event, Message::Plain);
157 }
158
159
160 void CtcpParser::parse(IrcEventRawMessage *e, Message::Type messagetype)
161 {
162     //lowlevel message dequote
163     QByteArray dequotedMessage = lowLevelDequote(e->rawMessage());
164
165     CtcpEvent::CtcpType ctcptype = e->type() == EventManager::IrcEventRawNotice
166                                    ? CtcpEvent::Reply
167                                    : CtcpEvent::Query;
168
169     Message::Flags flags = (ctcptype == CtcpEvent::Reply && !e->network()->isChannelName(e->target()))
170                            ? Message::Redirected
171                            : Message::None;
172
173     if (coreSession()->networkConfig()->standardCtcp())
174         parseStandard(e, messagetype, dequotedMessage, ctcptype, flags);
175     else
176         parseSimple(e, messagetype, dequotedMessage, ctcptype, flags);
177 }
178
179
180 // only accept CTCPs in their simplest form, i.e. one ctcp, from start to
181 // end, no text around it; not as per the 'specs', but makes people happier
182 void CtcpParser::parseSimple(IrcEventRawMessage *e, Message::Type messagetype, QByteArray dequotedMessage, CtcpEvent::CtcpType ctcptype, Message::Flags flags)
183 {
184     if (dequotedMessage.count(XDELIM) != 2 || dequotedMessage[0] != '\001' || dequotedMessage[dequotedMessage.count() -1] != '\001') {
185         displayMsg(e, messagetype, targetDecode(e, dequotedMessage), e->prefix(), e->target(), flags);
186     } else {
187         int spacePos = -1;
188         QString ctcpcmd, ctcpparam;
189
190         QByteArray ctcp = xdelimDequote(dequotedMessage.mid(1, dequotedMessage.count() - 2));
191         spacePos = ctcp.indexOf(' ');
192         if (spacePos != -1) {
193             ctcpcmd = targetDecode(e, ctcp.left(spacePos));
194             ctcpparam = targetDecode(e, ctcp.mid(spacePos + 1));
195         } else {
196             ctcpcmd = targetDecode(e, ctcp);
197             ctcpparam = QString();
198         }
199         ctcpcmd = ctcpcmd.toUpper();
200
201         // we don't want to block /me messages by the CTCP ignore list
202         if (ctcpcmd == QLatin1String("ACTION") || !coreSession()->ignoreListManager()->ctcpMatch(e->prefix(), e->network()->networkName(), ctcpcmd)) {
203             QUuid uuid = QUuid::createUuid();
204             _replies.insert(uuid, CtcpReply(coreNetwork(e), nickFromMask(e->prefix())));
205             CtcpEvent *event = new CtcpEvent(EventManager::CtcpEvent, e->network(), e->prefix(), e->target(),
206                 ctcptype, ctcpcmd, ctcpparam, e->timestamp(), uuid);
207             emit newEvent(event);
208             CtcpEvent *flushEvent = new CtcpEvent(EventManager::CtcpEventFlush, e->network(), e->prefix(), e->target(),
209                 ctcptype, "INVALID", QString(), e->timestamp(), uuid);
210             emit newEvent(flushEvent);
211         }
212     }
213 }
214
215
216 void CtcpParser::parseStandard(IrcEventRawMessage *e, Message::Type messagetype, QByteArray dequotedMessage, CtcpEvent::CtcpType ctcptype, Message::Flags flags)
217 {
218     QByteArray ctcp;
219
220     QList<CtcpEvent *> ctcpEvents;
221     QUuid uuid; // needed to group all replies together
222
223     // extract tagged / extended data
224     int xdelimPos = -1;
225     int xdelimEndPos = -1;
226     int spacePos = -1;
227     while ((xdelimPos = dequotedMessage.indexOf(XDELIM)) != -1) {
228         if (xdelimPos > 0)
229             displayMsg(e, messagetype, targetDecode(e, dequotedMessage.left(xdelimPos)), e->prefix(), e->target(), flags);
230
231         xdelimEndPos = dequotedMessage.indexOf(XDELIM, xdelimPos + 1);
232         if (xdelimEndPos == -1) {
233             // no matching end delimiter found... treat rest of the message as ctcp
234             xdelimEndPos = dequotedMessage.count();
235         }
236         ctcp = xdelimDequote(dequotedMessage.mid(xdelimPos + 1, xdelimEndPos - xdelimPos - 1));
237         dequotedMessage = dequotedMessage.mid(xdelimEndPos + 1);
238
239         //dispatch the ctcp command
240         QString ctcpcmd = targetDecode(e, ctcp.left(spacePos));
241         QString ctcpparam = targetDecode(e, ctcp.mid(spacePos + 1));
242
243         spacePos = ctcp.indexOf(' ');
244         if (spacePos != -1) {
245             ctcpcmd = targetDecode(e, ctcp.left(spacePos));
246             ctcpparam = targetDecode(e, ctcp.mid(spacePos + 1));
247         }
248         else {
249             ctcpcmd = targetDecode(e, ctcp);
250             ctcpparam = QString();
251         }
252
253         ctcpcmd = ctcpcmd.toUpper();
254
255         // we don't want to block /me messages by the CTCP ignore list
256         if (ctcpcmd == QLatin1String("ACTION") || !coreSession()->ignoreListManager()->ctcpMatch(e->prefix(), e->network()->networkName(), ctcpcmd)) {
257             if (uuid.isNull())
258                 uuid = QUuid::createUuid();
259
260             CtcpEvent *event = new CtcpEvent(EventManager::CtcpEvent, e->network(), e->prefix(), e->target(),
261                 ctcptype, ctcpcmd, ctcpparam, e->timestamp(), uuid);
262             ctcpEvents << event;
263         }
264     }
265     if (!ctcpEvents.isEmpty()) {
266         _replies.insert(uuid, CtcpReply(coreNetwork(e), nickFromMask(e->prefix())));
267         CtcpEvent *flushEvent = new CtcpEvent(EventManager::CtcpEventFlush, e->network(), e->prefix(), e->target(),
268             ctcptype, "INVALID", QString(), e->timestamp(), uuid);
269         ctcpEvents << flushEvent;
270         foreach(CtcpEvent *event, ctcpEvents) {
271             emit newEvent(event);
272         }
273     }
274
275     if (!dequotedMessage.isEmpty())
276         displayMsg(e, messagetype, targetDecode(e, dequotedMessage), e->prefix(), e->target(), flags);
277 }
278
279
280 void CtcpParser::sendCtcpEvent(CtcpEvent *e)
281 {
282     CoreNetwork *net = coreNetwork(e);
283     if (e->type() == EventManager::CtcpEvent) {
284         QByteArray quotedReply;
285         QString bufname = nickFromMask(e->prefix());
286         if (e->ctcpType() == CtcpEvent::Query && !e->reply().isNull()) {
287             if (_replies.contains(e->uuid()))
288                 _replies[e->uuid()].replies << lowLevelQuote(pack(net->serverEncode(e->ctcpCmd()),
289                         net->userEncode(bufname, e->reply())));
290             else
291                 // reply not caused by a request processed in here, so send it off immediately
292                 reply(net, bufname, e->ctcpCmd(), e->reply());
293         }
294     }
295     else if (e->type() == EventManager::CtcpEventFlush && _replies.contains(e->uuid())) {
296         CtcpReply reply = _replies.take(e->uuid());
297         if (reply.replies.count())
298             packedReply(net, reply.bufferName, reply.replies);
299     }
300 }
301
302
303 QByteArray CtcpParser::pack(const QByteArray &ctcpTag, const QByteArray &message)
304 {
305     if (message.isEmpty())
306         return XDELIM + ctcpTag + XDELIM;
307
308     return XDELIM + ctcpTag + ' ' + xdelimQuote(message) + XDELIM;
309 }
310
311
312 void CtcpParser::query(CoreNetwork *net, const QString &bufname, const QString &ctcpTag, const QString &message)
313 {
314     QList<QByteArray> params;
315     params << net->serverEncode(bufname) << lowLevelQuote(pack(net->serverEncode(ctcpTag), net->userEncode(bufname, message)));
316     net->putCmd("PRIVMSG", params);
317 }
318
319
320 void CtcpParser::reply(CoreNetwork *net, const QString &bufname, const QString &ctcpTag, const QString &message)
321 {
322     QList<QByteArray> params;
323     params << net->serverEncode(bufname) << lowLevelQuote(pack(net->serverEncode(ctcpTag), net->userEncode(bufname, message)));
324     net->putCmd("NOTICE", params);
325 }
326
327
328 void CtcpParser::packedReply(CoreNetwork *net, const QString &bufname, const QList<QByteArray> &replies)
329 {
330     QList<QByteArray> params;
331
332     int answerSize = 0;
333     for (int i = 0; i < replies.count(); i++) {
334         answerSize += replies.at(i).size();
335     }
336
337     QByteArray quotedReply;
338     quotedReply.reserve(answerSize);
339     for (int i = 0; i < replies.count(); i++) {
340         quotedReply.append(replies.at(i));
341     }
342
343     params << net->serverEncode(bufname) << quotedReply;
344     // FIXME user proper event
345     net->putCmd("NOTICE", params);
346 }