src: Yearly copyright bump
[quassel.git] / src / core / eventstringifier.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2019 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 "eventstringifier.h"
22
23 #include "coresession.h"
24 #include "ctcpevent.h"
25 #include "messageevent.h"
26
27 EventStringifier::EventStringifier(CoreSession* parent)
28     : BasicHandler("handleCtcp", parent)
29     , _coreSession(parent)
30     , _whois(false)
31 {
32     connect(this, &EventStringifier::newMessageEvent, coreSession()->eventManager(), &EventManager::postEvent);
33 }
34
35 void EventStringifier::displayMsg(
36     NetworkEvent* event, Message::Type msgType, const QString& msg, const QString& sender, const QString& target, Message::Flags msgFlags)
37 {
38     if (event->flags().testFlag(EventManager::Silent))
39         return;
40
41     MessageEvent* msgEvent = createMessageEvent(event, msgType, msg, sender, target, msgFlags);
42     // sendMessageEvent(msgEvent);
43     emit newMessageEvent(msgEvent);
44 }
45
46 MessageEvent* EventStringifier::createMessageEvent(
47     NetworkEvent* event, Message::Type msgType, const QString& msg, const QString& sender, const QString& target, Message::Flags msgFlags)
48 {
49     MessageEvent* msgEvent = new MessageEvent(msgType, event->network(), msg, sender, target, msgFlags);
50     msgEvent->setTimestamp(event->timestamp());
51     return msgEvent;
52 }
53
54 bool EventStringifier::checkParamCount(IrcEvent* e, int minParams)
55 {
56     if (e->params().count() < minParams) {
57         if (e->type() == EventManager::IrcEventNumeric) {
58             qWarning() << "Command " << static_cast<IrcEventNumeric*>(e)->number() << " requires " << minParams
59                        << "params, got: " << e->params();
60         }
61         else {
62             QString name = coreSession()->eventManager()->enumName(e->type());
63             qWarning() << qPrintable(name) << "requires" << minParams << "params, got:" << e->params();
64         }
65         e->stop();
66         return false;
67     }
68     return true;
69 }
70
71 /* These are only for legacy reasons; remove as soon as we handle NetworkSplitEvents properly */
72 void EventStringifier::processNetworkSplitJoin(NetworkSplitEvent* e)
73 {
74     QString msg = e->users().join("#:#") + "#:#" + e->quitMessage();
75     displayMsg(e, Message::NetsplitJoin, msg, QString(), e->channel());
76 }
77
78 void EventStringifier::processNetworkSplitQuit(NetworkSplitEvent* e)
79 {
80     QString msg = e->users().join("#:#") + "#:#" + e->quitMessage();
81     displayMsg(e, Message::NetsplitQuit, msg, QString(), e->channel());
82 }
83
84 /* End legacy */
85
86 void EventStringifier::processIrcEventNumeric(IrcEventNumeric* e)
87 {
88     // qDebug() << e->number();
89     switch (e->number()) {
90     // Welcome, status, info messages. Just display these.
91     case 1:
92     case 2:
93     case 3:
94     case 4:
95     case 5:
96     case 221:
97     case 250:
98     case 251:
99     case 252:
100     case 253:
101     case 254:
102     case 255:
103     case 256:
104     case 257:
105     case 258:
106     case 259:
107     case 265:
108     case 266:
109     case 372:
110     case 375:
111         displayMsg(e, Message::Server, e->params().join(" "), e->prefix());
112         break;
113
114     // Server error messages without param, just display them
115     case 263:
116     case 409:
117     case 411:
118     case 412:
119     case 422:
120     case 424:
121     case 445:
122     case 446:
123     case 451:
124     case 462:
125     case 463:
126     case 464:
127     case 465:
128     case 466:
129     case 472:
130     case 481:
131     case 483:
132     case 485:
133     case 491:
134     case 501:
135     case 502:
136     case 431:  // ERR_NONICKNAMEGIVEN
137         displayMsg(e, Message::Error, e->params().join(" "), e->prefix());
138         break;
139
140     // Server error messages, display them in red. Colon between first param and rest.
141     case 401: {
142         if (!checkParamCount(e, 1))
143             return;
144
145         QStringList params = e->params();
146         QString target = params.takeFirst();
147         displayMsg(e, Message::Error, target + ": " + params.join(" "), e->prefix(), target, Message::Redirected);
148         break;
149     }
150
151     case 402:
152     case 403:
153     case 404:
154     case 406:
155     case 408:
156     case 415:
157     case 421:
158     case 442: {
159         if (!checkParamCount(e, 1))
160             return;
161
162         QStringList params = e->params();
163         QString channelName = params.takeFirst();
164         displayMsg(e, Message::Error, channelName + ": " + params.join(" "), e->prefix());
165         break;
166     }
167
168     // Server error messages which will be displayed with a colon between the first param and the rest
169     case 413:
170     case 414:
171     case 423:
172     case 441:
173     case 444:
174     case 461:  // FIXME see below for the 47x codes
175     case 467:
176     case 471:
177     case 473:
178     case 474:
179     case 475:
180     case 476:
181     case 477:
182     case 478:
183     case 482:
184     case 436:  // ERR_NICKCOLLISION
185     {
186         if (!checkParamCount(e, 1))
187             return;
188
189         QStringList params = e->params();
190         QString p = params.takeFirst();
191         displayMsg(e, Message::Error, p + ": " + params.join(" "));
192         break;
193     }
194
195     // Ignore these commands.
196     case 321:
197     case 353:
198     case 366:
199     case 376:
200         break;
201
202     // SASL authentication stuff
203     // See: http://ircv3.net/specs/extensions/sasl-3.1.html
204     case 900:  // RPL_LOGGEDIN
205     case 901:  // RPL_LOGGEDOUT
206     {
207         // :server 900 <nick> <nick>!<ident>@<host> <account> :You are now logged in as <user>
208         // :server 901 <nick> <nick>!<ident>@<host> :You are now logged out
209         if (!checkParamCount(e, 3))
210             return;
211         displayMsg(e, Message::Server, "SASL: " + e->params().at(2));
212         break;
213     }
214     // Ignore SASL success, partially redundant with RPL_LOGGEDIN and RPL_LOGGEDOUT
215     case 903:  // RPL_SASLSUCCESS  :server 903 <nick> :SASL authentication successful
216         break;
217     case 902:  // ERR_NICKLOCKED   :server 902 <nick> :You must use a nick assigned to you
218     case 904:  // ERR_SASLFAIL     :server 904 <nick> :SASL authentication failed
219     case 905:  // ERR_SASLTOOLONG  :server 905 <nick> :SASL message too long
220     case 906:  // ERR_SASLABORTED  :server 906 <nick> :SASL authentication aborted
221     case 907:  // ERR_SASLALREADY  :server 907 <nick> :You have already authenticated using SASL
222     case 908:  // RPL_SASLMECHS    :server 908 <nick> <mechanisms> :are available SASL mechanisms
223     {
224         displayMsg(e, Message::Server, "SASL: " + e->params().join(""));
225         break;
226     }
227
228     // Everything else will be marked in red, so we can add them somewhere.
229     default:
230         if (_whois) {
231             // many nets define their own WHOIS fields. we fetch those not in need of special attention here:
232             displayMsg(e, Message::Server, tr("[Whois] ") + e->params().join(" "), e->prefix());
233         }
234         else {
235             // FIXME figure out how/where to do this in the future
236             // if(coreSession()->ircListHelper()->requestInProgress(network()->networkId()))
237             //  coreSession()->ircListHelper()->reportError(params.join(" "));
238             // else
239             displayMsg(e, Message::Error, QString("%1 %2").arg(e->number(), 3, 10, QLatin1Char('0')).arg(e->params().join(" ")), e->prefix());
240         }
241     }
242 }
243
244 void EventStringifier::processIrcEventInvite(IrcEvent* e)
245 {
246     displayMsg(e, Message::Invite, tr("%1 invited you to channel %2").arg(e->nick(), e->params().at(1)));
247 }
248
249 void EventStringifier::processIrcEventJoin(IrcEvent* e)
250 {
251     if (e->testFlag(EventManager::Netsplit))
252         return;
253
254     Message::Flag msgFlags = Message::Flag::None;
255     if (e->testFlag(EventManager::Self)) {
256         // Mark the message as Self
257         msgFlags = Message::Self;
258     }
259
260     displayMsg(e, Message::Join, e->params()[0], e->prefix(), e->params()[0], msgFlags);
261 }
262
263 void EventStringifier::processIrcEventKick(IrcEvent* e)
264 {
265     if (!checkParamCount(e, 2))
266         return;
267
268     IrcUser* victim = e->network()->ircUser(e->params().at(1));
269     if (victim) {
270         QString channel = e->params().at(0);
271         QString msg = victim->nick();
272         if (e->params().count() > 2)
273             msg += " " + e->params().at(2);
274
275         Message::Flag msgFlags = Message::Flag::None;
276         if (e->testFlag(EventManager::Self)) {
277             // Mark the message as Self
278             msgFlags = Message::Self;
279         }
280
281         displayMsg(e, Message::Kick, msg, e->prefix(), channel, msgFlags);
282     }
283 }
284
285 void EventStringifier::processIrcEventMode(IrcEvent* e)
286 {
287     if (e->network()->isChannelName(e->params().first())) {
288         // Channel Modes
289         displayMsg(e, Message::Mode, e->params().join(" "), e->prefix(), e->params().first());
290     }
291     else {
292         // User Modes
293         // FIXME: redirect
294
295         Message::Flag msgFlags = Message::Flag::None;
296         if (e->testFlag(EventManager::Self)) {
297             // Mark the message as Self
298             msgFlags = Message::Self;
299         }
300         displayMsg(e, Message::Mode, e->params().join(" "), e->prefix(), QString(), msgFlags);
301     }
302 }
303
304 // this needs to be called before the ircuser is renamed!
305 void EventStringifier::processIrcEventNick(IrcEvent* e)
306 {
307     if (!checkParamCount(e, 1))
308         return;
309
310     IrcUser* ircuser = e->network()->updateNickFromMask(e->prefix());
311     if (!ircuser) {
312         qWarning() << Q_FUNC_INFO << "Unknown IrcUser!";
313         return;
314     }
315
316     QString newnick = e->params().at(0);
317
318     QString sender;
319     Message::Flag msgFlags = Message::Flag::None;
320     if (e->testFlag(EventManager::Self)) {
321         // Treat the sender as the new nickname, mark the message as Self
322         sender = newnick;
323         msgFlags = Message::Self;
324     }
325     else {
326         // Take the sender from the event prefix, don't mark the message
327         sender = e->prefix();
328     }
329
330     // Announce to all channels the IrcUser is in
331     foreach (const QString& channel, ircuser->channels()) {
332         displayMsg(e, Message::Nick, newnick, sender, channel, msgFlags);
333     }
334 }
335
336 void EventStringifier::processIrcEventPart(IrcEvent* e)
337 {
338     if (!checkParamCount(e, 1))
339         return;
340
341     QString channel = e->params().at(0);
342     QString msg = e->params().count() > 1 ? e->params().at(1) : QString();
343
344     Message::Flag msgFlags = Message::Flag::None;
345     if (e->testFlag(EventManager::Self)) {
346         // Mark the message as Self
347         msgFlags = Message::Self;
348     }
349
350     displayMsg(e, Message::Part, msg, e->prefix(), channel, msgFlags);
351 }
352
353 void EventStringifier::processIrcEventPong(IrcEvent* e)
354 {
355     // CoreSessionEventProcessor will flag automated PONG replies as EventManager::Silent.  There's
356     // no need to handle that specially here.
357
358     // Format the PONG reply for display
359     displayMsg(e, Message::Server, "PONG " + e->params().join(" "), e->prefix());
360 }
361
362 void EventStringifier::processIrcEventQuit(IrcEvent* e)
363 {
364     if (e->testFlag(EventManager::Netsplit))
365         return;
366
367     IrcUser* ircuser = e->network()->updateNickFromMask(e->prefix());
368     if (!ircuser)
369         return;
370
371     Message::Flag msgFlags = Message::Flag::None;
372     if (e->testFlag(EventManager::Self)) {
373         // Mark the message as Self
374         msgFlags = Message::Self;
375     }
376
377     // Announce to all channels the IrcUser is in
378     foreach (const QString& channel, ircuser->channels()) {
379         displayMsg(e, Message::Quit, e->params().count() ? e->params().first() : QString(), e->prefix(), channel, msgFlags);
380     }
381 }
382
383 void EventStringifier::processIrcEventTopic(IrcEvent* e)
384 {
385     Message::Flag msgFlags = Message::Flag::None;
386     if (e->testFlag(EventManager::Self)) {
387         // Mark the message as Self
388         msgFlags = Message::Self;
389     }
390
391     displayMsg(e,
392                Message::Topic,
393                tr("%1 has changed topic for %2 to: \"%3\"").arg(e->nick(), e->params().at(0), e->params().at(1)),
394                QString(),
395                e->params().at(0),
396                msgFlags);
397 }
398
399 void EventStringifier::processIrcEventError(IrcEvent* e)
400 {
401     // Need an error reason
402     if (!checkParamCount(e, 1))
403         return;
404
405     displayMsg(e, Message::Server, tr("Error from server: ") + e->params().join(""));
406 }
407
408 void EventStringifier::processIrcEventWallops(IrcEvent* e)
409 {
410     displayMsg(e, Message::Server, tr("[Operwall] %1: %2").arg(e->nick(), e->params().join(" ")));
411 }
412
413 /* RPL_ISUPPORT */
414 void EventStringifier::processIrcEvent005(IrcEvent* e)
415 {
416     if (!e->params().last().contains(QRegExp("are supported (by|on) this server")))
417         displayMsg(e, Message::Error, tr("Received non-RFC-compliant RPL_ISUPPORT: this can lead to unexpected behavior!"), e->prefix());
418     displayMsg(e, Message::Server, e->params().join(" "), e->prefix());
419 }
420
421 /* RPL_AWAY - "<nick> :<away message>" */
422 void EventStringifier::processIrcEvent301(IrcEvent* e)
423 {
424     QString nick = e->params().at(0);
425     QString awayMsg = e->params().at(1);
426     QString msg, target;
427     bool send = true;
428
429     // FIXME: proper redirection needed
430     if (_whois) {
431         msg = tr("[Whois] ");
432     }
433     else {
434         target = nick;
435         IrcUser* ircuser = e->network()->ircUser(nick);
436         if (ircuser) {
437             QDateTime now = QDateTime::currentDateTime();
438             now.setTimeSpec(Qt::UTC);
439             // Don't print "user is away" messages more often than this
440             // 1 hour = 60 min * 60 sec
441             const int silenceTime = 60 * 60;
442             // Check if away state has NOT changed and silence time hasn't yet elapsed
443             if (!ircuser->hasAwayChanged() && ircuser->lastAwayMessageTime().addSecs(silenceTime) >= now) {
444                 // Away message hasn't changed and we're still within the period of silence; don't
445                 // repeat the message
446                 send = false;
447             }
448             ircuser->setLastAwayMessageTime(now);
449             // Mark any changes in away as acknowledged
450             ircuser->acknowledgeAwayChanged();
451         }
452     }
453     if (send)
454         displayMsg(e, Message::Server, msg + tr("%1 is away: \"%2\"").arg(nick, awayMsg), QString(), target);
455 }
456
457 /* RPL_UNAWAY - ":You are no longer marked as being away" */
458 void EventStringifier::processIrcEvent305(IrcEvent* e)
459 {
460     displayMsg(e, Message::Server, tr("You are no longer marked as being away"));
461 }
462
463 /* RPL_NOWAWAY - ":You have been marked as being away" */
464 void EventStringifier::processIrcEvent306(IrcEvent* e)
465 {
466     if (!e->network()->autoAwayActive())
467         displayMsg(e, Message::Server, tr("You have been marked as being away"));
468 }
469
470 /*
471 WHOIS-Message:
472    Replies 311 - 313, 317 - 319 are all replies generated in response to a WHOIS message.
473   and 301 (RPL_AWAY)
474               "<nick> :<away message>"
475 WHO-Message:
476    Replies 352 and 315 paired are used to answer a WHO message.
477
478 WHOWAS-Message:
479    Replies 314 and 369 are responses to a WHOWAS message.
480
481 */
482
483 /*  RPL_WHOISUSER - "<nick> <user> <host> * :<real name>" */
484 void EventStringifier::processIrcEvent311(IrcEvent* e)
485 {
486     _whois = true;
487
488     const QString whoisUserString = tr("[Whois] %1 is %2 (%3)");
489
490     IrcUser* ircuser = e->network()->ircUser(e->params().at(0));
491     if (ircuser)
492         displayMsg(e, Message::Server, whoisUserString.arg(ircuser->nick(), ircuser->hostmask(), ircuser->realName()));
493     else {
494         QString host = QString("%1!%2@%3").arg(e->params().at(0), e->params().at(1), e->params().at(2));
495         displayMsg(e, Message::Server, whoisUserString.arg(e->params().at(0), host, e->params().last()));
496     }
497 }
498
499 /*  RPL_WHOISSERVER -  "<nick> <server> :<server info>" */
500 void EventStringifier::processIrcEvent312(IrcEvent* e)
501 {
502     if (_whois)
503         displayMsg(e, Message::Server, tr("[Whois] %1 is online via %2 (%3)").arg(e->params().at(0), e->params().at(1), e->params().last()));
504     else
505         displayMsg(e, Message::Server, tr("[Whowas] %1 was online via %2 (%3)").arg(e->params().at(0), e->params().at(1), e->params().last()));
506 }
507
508 /*  RPL_WHOWASUSER - "<nick> <user> <host> * :<real name>" */
509 void EventStringifier::processIrcEvent314(IrcEvent* e)
510 {
511     if (!checkParamCount(e, 3))
512         return;
513
514     displayMsg(e, Message::Server, tr("[Whowas] %1 was %2@%3 (%4)").arg(e->params()[0], e->params()[1], e->params()[2], e->params().last()));
515 }
516
517 /*  RPL_ENDOFWHO: "<name> :End of WHO list" */
518 void EventStringifier::processIrcEvent315(IrcEvent* e)
519 {
520     QStringList p = e->params();
521     p.takeLast();  // should be "End of WHO list"
522     displayMsg(e, Message::Server, tr("[Who] End of /WHO list for %1").arg(p.join(" ")));
523 }
524
525 /*  RPL_WHOISIDLE - "<nick> <integer> :seconds idle"
526    (real life: "<nick> <integer> <integer> :seconds idle, signon time) */
527 void EventStringifier::processIrcEvent317(IrcEvent* e)
528 {
529     int idleSecs = e->params()[1].toInt();
530
531     if (e->params().count() > 3) {  // if we have more then 3 params we have the above mentioned "real life" situation
532                                     // Time in IRC protocol is defined as seconds.  Convert from seconds instead.
533                                     // See https://doc.qt.io/qt-5/qdatetime.html#fromSecsSinceEpoch
534 #if QT_VERSION >= 0x050800
535         QDateTime loginTime = QDateTime::fromSecsSinceEpoch(e->params()[2].toLongLong()).toUTC();
536 #else
537         // fromSecsSinceEpoch() was added in Qt 5.8.  Manually downconvert to seconds for now.
538         // See https://doc.qt.io/qt-5/qdatetime.html#fromMSecsSinceEpoch
539         QDateTime loginTime = QDateTime::fromMSecsSinceEpoch((qint64)(e->params()[2].toLongLong() * 1000)).toUTC();
540 #endif
541         displayMsg(e,
542                    Message::Server,
543                    tr("[Whois] %1 is logged in since %2").arg(e->params()[0], loginTime.toString("yyyy-MM-dd hh:mm:ss UTC")));
544     }
545     QDateTime idlingSince = e->timestamp().toLocalTime().addSecs(-idleSecs).toUTC();
546     displayMsg(e,
547                Message::Server,
548                tr("[Whois] %1 is idling for %2 (since %3)")
549                    .arg(e->params()[0], secondsToString(idleSecs), idlingSince.toString("yyyy-MM-dd hh:mm:ss UTC")));
550 }
551
552 /*  RPL_ENDOFWHOIS - "<nick> :End of WHOIS list" */
553 void EventStringifier::processIrcEvent318(IrcEvent* e)
554 {
555     _whois = false;
556     displayMsg(e, Message::Server, tr("[Whois] End of /WHOIS list"));
557 }
558
559 /*  RPL_WHOISCHANNELS - "<nick> :*( ( "@" / "+" ) <channel> " " )" */
560 void EventStringifier::processIrcEvent319(IrcEvent* e)
561 {
562     if (!checkParamCount(e, 2))
563         return;
564
565     QString nick = e->params().first();
566     QStringList op;
567     QStringList voice;
568     QStringList user;
569     foreach (QString channel, e->params().last().split(" ")) {
570         if (channel.startsWith("@"))
571             op.append(channel.remove(0, 1));
572         else if (channel.startsWith("+"))
573             voice.append(channel.remove(0, 1));
574         else
575             user.append(channel);
576     }
577     if (!user.isEmpty())
578         displayMsg(e, Message::Server, tr("[Whois] %1 is a user on channels: %2").arg(nick, user.join(" ")));
579     if (!voice.isEmpty())
580         displayMsg(e, Message::Server, tr("[Whois] %1 has voice on channels: %2").arg(nick, voice.join(" ")));
581     if (!op.isEmpty())
582         displayMsg(e, Message::Server, tr("[Whois] %1 is an operator on channels: %2").arg(nick, op.join(" ")));
583 }
584
585 /* RPL_LIST -  "<channel> <# visible> :<topic>" */
586 void EventStringifier::processIrcEvent322(IrcEvent* e)
587 {
588     QString channelName;
589     quint32 userCount = 0;
590     QString topic;
591
592     switch (e->params().count()) {
593     case 3:
594         topic = e->params()[2];
595         // fallthrough
596     case 2:
597         userCount = e->params()[1].toUInt();
598         /* fallthrough */
599     case 1:
600         channelName = e->params()[0];
601         // blubb
602     default:
603         break;
604     }
605     displayMsg(e, Message::Server, tr("Channel %1 has %2 users. Topic is: \"%3\"").arg(channelName).arg(userCount).arg(topic));
606 }
607
608 /* RPL_LISTEND ":End of LIST" */
609 void EventStringifier::processIrcEvent323(IrcEvent* e)
610 {
611     displayMsg(e, Message::Server, tr("End of channel list"));
612 }
613
614 /* RPL_CHANNELMODEIS - "<channel> <mode> <mode params>" */
615 void EventStringifier::processIrcEvent324(IrcEvent* e)
616 {
617     processIrcEventMode(e);
618 }
619
620 /* RPL_??? - "<channel> <homepage> */
621 void EventStringifier::processIrcEvent328(IrcEvent* e)
622 {
623     if (!checkParamCount(e, 2))
624         return;
625
626     QString channel = e->params()[0];
627     displayMsg(e, Message::Topic, tr("Homepage for %1 is %2").arg(channel, e->params()[1]), QString(), channel);
628 }
629
630 /* RPL_??? - "<channel> <creation time (unix)>" */
631 void EventStringifier::processIrcEvent329(IrcEvent* e)
632 {
633     if (!checkParamCount(e, 2))
634         return;
635
636     QString channel = e->params()[0];
637     // Allow for 64-bit time
638     qint64 unixtime = e->params()[1].toLongLong();
639     if (!unixtime) {
640         qWarning() << Q_FUNC_INFO << "received invalid timestamp:" << e->params()[1];
641         return;
642     }
643     // Time in IRC protocol is defined as seconds.  Convert from seconds instead.
644     // See https://doc.qt.io/qt-5/qdatetime.html#fromSecsSinceEpoch
645 #if QT_VERSION >= 0x050800
646     QDateTime time = QDateTime::fromSecsSinceEpoch(unixtime).toUTC();
647 #else
648     // fromSecsSinceEpoch() was added in Qt 5.8.  Manually downconvert to seconds for now.
649     // See https://doc.qt.io/qt-5/qdatetime.html#fromMSecsSinceEpoch
650     QDateTime time = QDateTime::fromMSecsSinceEpoch((qint64)(unixtime * 1000)).toUTC();
651 #endif
652     displayMsg(e, Message::Topic, tr("Channel %1 created on %2").arg(channel, time.toString("yyyy-MM-dd hh:mm:ss UTC")), QString(), channel);
653 }
654
655 /*  RPL_WHOISACCOUNT: "<nick> <account> :is authed as */
656 void EventStringifier::processIrcEvent330(IrcEvent* e)
657 {
658     if (e->params().count() < 3)
659         return;
660
661     // check for whois or whowas
662     if (_whois) {
663         displayMsg(e, Message::Server, tr("[Whois] %1 is authed as %2").arg(e->params()[0], e->params()[1]));
664     }
665     else {
666         displayMsg(e, Message::Server, tr("[Whowas] %1 was authed as %2").arg(e->params()[0], e->params()[1]));
667     }
668 }
669
670 /* RPL_NOTOPIC */
671 void EventStringifier::processIrcEvent331(IrcEvent* e)
672 {
673     QString channel = e->params().first();
674     displayMsg(e, Message::Topic, tr("No topic is set for %1.").arg(channel), QString(), channel);
675 }
676
677 /* RPL_TOPIC */
678 void EventStringifier::processIrcEvent332(IrcEvent* e)
679 {
680     QString channel = e->params().first();
681     displayMsg(e, Message::Topic, tr("Topic for %1 is \"%2\"").arg(channel, e->params()[1]), QString(), channel);
682 }
683
684 /* Topic set by... */
685 void EventStringifier::processIrcEvent333(IrcEvent* e)
686 {
687     if (!checkParamCount(e, 3))
688         return;
689
690     QString channel = e->params().first();
691     // Time in IRC protocol is defined as seconds.  Convert from seconds instead.
692     // See https://doc.qt.io/qt-5/qdatetime.html#fromSecsSinceEpoch
693 #if QT_VERSION >= 0x050800
694     QDateTime topicSetTime = QDateTime::fromSecsSinceEpoch(e->params()[2].toLongLong()).toUTC();
695 #else
696     // fromSecsSinceEpoch() was added in Qt 5.8.  Manually downconvert to seconds for now.
697     // See https://doc.qt.io/qt-5/qdatetime.html#fromMSecsSinceEpoch
698     QDateTime topicSetTime = QDateTime::fromMSecsSinceEpoch((qint64)(e->params()[2].toLongLong() * 1000)).toUTC();
699 #endif
700     displayMsg(e,
701                Message::Topic,
702                tr("Topic set by %1 on %2").arg(e->params()[1], topicSetTime.toString("yyyy-MM-dd hh:mm:ss UTC")),
703                QString(),
704                channel);
705 }
706
707 /* RPL_INVITING - "<nick> <channel>*/
708 void EventStringifier::processIrcEvent341(IrcEvent* e)
709 {
710     if (!checkParamCount(e, 2))
711         return;
712
713     QString channel = e->params()[1];
714     displayMsg(e, Message::Server, tr("%1 has been invited to %2").arg(e->params().first(), channel), QString(), channel);
715 }
716
717 /*  RPL_WHOREPLY: "<channel> <user> <host> <server> <nick>
718               ( "H" / "G" > ["*"] [ ( "@" / "+" ) ] :<hopcount> <real name>" */
719 void EventStringifier::processIrcEvent352(IrcEvent* e)
720 {
721     displayMsg(e, Message::Server, tr("[Who] %1").arg(e->params().join(" ")));
722 }
723
724 /*  RPL_WHOSPCRPL: "<yournick> <num> #<channel> ~<ident> <host> <servname> <nick>
725                     ("H"/ "G") <account> :<realname>"
726 Could be anything else, though.  User-specified fields.
727 See http://faerion.sourceforge.net/doc/irc/whox.var */
728 void EventStringifier::processIrcEvent354(IrcEvent* e)
729 {
730     displayMsg(e, Message::Server, tr("[WhoX] %1").arg(e->params().join(" ")));
731 }
732
733 /*  RPL_ENDOFWHOWAS - "<nick> :End of WHOWAS" */
734 void EventStringifier::processIrcEvent369(IrcEvent* e)
735 {
736     displayMsg(e, Message::Server, tr("End of /WHOWAS"));
737 }
738
739 /* ERR_ERRONEUSNICKNAME */
740 void EventStringifier::processIrcEvent432(IrcEvent* e)
741 {
742     if (!checkParamCount(e, 1))
743         return;
744
745     displayMsg(e, Message::Error, tr("Nick %1 contains illegal characters").arg(e->params()[0]));
746 }
747
748 /* ERR_NICKNAMEINUSE */
749 void EventStringifier::processIrcEvent433(IrcEvent* e)
750 {
751     if (!checkParamCount(e, 1))
752         return;
753
754     displayMsg(e, Message::Error, tr("Nick already in use: %1").arg(e->params()[0]));
755 }
756
757 /* ERR_UNAVAILRESOURCE */
758 void EventStringifier::processIrcEvent437(IrcEvent* e)
759 {
760     if (!checkParamCount(e, 1))
761         return;
762
763     displayMsg(e, Message::Error, tr("Nick/channel is temporarily unavailable: %1").arg(e->params()[0]));
764 }
765
766 // template
767 /*
768
769 void EventStringifier::processIrcEvent(IrcEvent *e) {
770
771 }
772
773 */
774
775 /*******************************/
776 /******** CTCP HANDLING ********/
777 /*******************************/
778
779 void EventStringifier::processCtcpEvent(CtcpEvent* e)
780 {
781     if (e->type() != EventManager::CtcpEvent)
782         return;
783
784     if (e->testFlag(EventManager::Self)) {
785         displayMsg(e,
786                    Message::Action,
787                    tr("sending CTCP-%1 request to %2").arg(e->ctcpCmd(), e->target()),
788                    e->network()->myNick(),
789                    QString(),
790                    Message::Flag::Self);
791         return;
792     }
793
794     handle(e->ctcpCmd(), Q_ARG(CtcpEvent*, e));
795 }
796
797 void EventStringifier::defaultHandler(const QString& ctcpCmd, CtcpEvent* e)
798 {
799     Q_UNUSED(ctcpCmd);
800     if (e->ctcpType() == CtcpEvent::Query) {
801         QString unknown;
802         if (e->reply().isNull())  // all known core-side handlers (except for ACTION) set a reply!
803             //: Optional "unknown" in "Received unknown CTCP-FOO request by bar"
804             unknown = tr("unknown") + ' ';
805         displayMsg(e, Message::Server, tr("Received %1CTCP-%2 request by %3").arg(unknown, e->ctcpCmd(), e->prefix()));
806         return;
807     }
808     displayMsg(e, Message::Server, tr("Received CTCP-%1 answer from %2: %3").arg(e->ctcpCmd(), nickFromMask(e->prefix()), e->param()));
809 }
810
811 void EventStringifier::handleCtcpAction(CtcpEvent* e)
812 {
813     displayMsg(e, Message::Action, e->param(), e->prefix(), e->target());
814 }
815
816 void EventStringifier::handleCtcpPing(CtcpEvent* e)
817 {
818     if (e->ctcpType() == CtcpEvent::Query)
819         defaultHandler(e->ctcpCmd(), e);
820     else {
821         displayMsg(e,
822                    Message::Server,
823                    tr("Received CTCP-PING answer from %1 with %2 milliseconds round trip time")
824                        .arg(nickFromMask(e->prefix()))
825                        .arg(QDateTime::fromMSecsSinceEpoch(e->param().toULongLong()).msecsTo(e->timestamp())));
826     }
827 }