ircresponseparser.cpp
1 //------------------------------------------------------------------------------
2 // ircresponseparser.cpp
3 //------------------------------------------------------------------------------
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library 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 GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 // 02110-1301 USA
19 //
20 //------------------------------------------------------------------------------
21 // Copyright (C) 2010 "Zalewa" <zalewapl@gmail.com>
22 //------------------------------------------------------------------------------
23 #include "ircresponseparser.h"
24 
25 #include "irc/constants/ircresponsetype.h"
26 #include "irc/ircctcpparser.h"
27 #include "irc/ircglobal.h"
28 #include "irc/ircmessageclass.h"
29 #include "irc/ircnetworkadapter.h"
30 #include "irc/ircuserinfo.h"
31 #include "log.h"
32 #include "patternlist.h"
33 #include "strings.hpp"
34 #include <cassert>
35 #include <QDateTime>
36 #include <QRegularExpression>
37 #include <QStringList>
38 
39 DClass<IRCResponseParser>
40 {
41 public:
42  IRCNetworkAdapter *network;
43  QString prefix;
44  QString sender;
45  QString type;
46  QStringList params;
47 };
48 
49 DPointered(IRCResponseParser)
50 
52 {
53  d->network = network;
54 }
55 
56 IRCResponseParser::~IRCResponseParser()
57 {
58 }
59 
60 IRCResponseParser::FlagModes IRCResponseParser::getFlagMode(char c)
61 {
62  switch (c)
63  {
64  case '+':
65  return FlagModeAdd;
66 
67  case '-':
68  return FlagModeRemove;
69 
70  default:
71  return FlagModeError;
72  }
73 }
74 
75 bool IRCResponseParser::isPrefixIgnored() const
76 {
77  return d->network->ignoredUsersPatterns().isExactMatchAny(d->prefix);
78 }
79 
80 QString IRCResponseParser::joinAndTrimColonIfNecessary(const QStringList &strList) const
81 {
82  QString joined = strList.join(" ");
83  return this->trimColonIfNecessary(joined);
84 }
85 
87 {
88  QString formattedMessage = message.trimmed();
89 
90  QRegularExpression prefixRegExp("(^:\\S+\\s)(.*)");
91  auto match = prefixRegExp.match(formattedMessage);
92 
93  QString prefix = match.captured(1);
94  QString remainder = formattedMessage.mid(prefix.length());
95 
96  d->prefix = Strings::triml(prefix, ":").trimmed();
97 
98  // Obtain message sender from the prefix.
99  int indexExclamation = prefix.indexOf('!');
100  if (indexExclamation > 0)
101  d->sender = d->prefix.left(indexExclamation);
102  else
103  d->sender = d->prefix;
104 
105  QStringList msgParameters = remainder.split(" ");
106  if (!msgParameters.isEmpty())
107  {
108  d->type = msgParameters.takeFirst();
109  d->params = msgParameters;
110 
111  return parseMessage();
112  }
113 
114  // Return invalid result.
115  return IRCResponseParseResult();
116 }
117 
118 IRCResponseParseResult IRCResponseParser::parseMessage()
119 {
120  IRCResponseType responseType(d->type);
121  IRCResponseType::MsgType enumType = responseType.type();
122 
123  switch (enumType)
124  {
126  {
127  QString nickname = d->params.takeFirst();
128 
129  emit helloClient(nickname);
130  break;
131  }
132 
134  {
135  d->params.takeFirst(); // Own nick.
136  QString nickname = d->params.takeFirst();
137  QString reason = joinAndTrimColonIfNecessary(d->params);
138  emit printToNetworksCurrentChatBox(tr("User %1 is away: %2").arg(nickname, reason),
139  IRCMessageClass::NetworkAction);
140  break;
141  }
142 
144  {
145  // First param is unnecessary
146  d->params.takeFirst();
147 
148  // Extract user info.
149  QString nickname = d->params.takeFirst();
150  QString user = d->params.takeFirst();
151  QString hostName = d->params.takeFirst();
152  QString realName = joinAndTrimColonIfNecessary(d->params);
153 
154  emit whoIsUser(nickname, user, hostName, realName);
155  break;
156  }
157 
168  {
169  d->params.takeFirst(); // Own nick.
170  emit printToNetworksCurrentChatBox(d->params.join(" "), IRCMessageClass::NetworkAction);
171  break;
172  }
173 
175  {
176  d->params.takeFirst(); // Own nick.
177  QString nick = d->params.takeFirst();
178  int secondsIdle = d->params.takeFirst().toInt();
179  emit userIdleTime(nick, secondsIdle);
180  if (d->params.first().toInt() != 0)
181  {
182  int joinedOn = d->params.takeFirst().toInt();
183 #if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
184  emit userNetworkJoinDateTime(nick, QDateTime::fromSecsSinceEpoch(joinedOn));
185 #else
186  emit userNetworkJoinDateTime(nick, QDateTime::fromTime_t(joinedOn));
187 #endif
188  }
189  break;
190  }
191 
193  {
194  d->params.takeFirst(); // Own nick.
195  QString nick = d->params.takeFirst();
196  QString channels = joinAndTrimColonIfNecessary(d->params);
197  emit printToNetworksCurrentChatBox(tr("%1 is on channels: %2").arg(nick, channels),
198  IRCMessageClass::NetworkAction);
199  break;
200  }
201 
203  {
204  d->params.takeFirst(); // Own nick.
205  QString nick = d->params.takeFirst();
206  QString account = d->params.takeFirst();
207  QString message = joinAndTrimColonIfNecessary(d->params);
208  emit printToNetworksCurrentChatBox(QString("%1 %2 %3").arg(nick, message, account),
209  IRCMessageClass::NetworkAction);
210  break;
211  }
212 
214  {
215  d->params.takeFirst(); // Own nickname.
216  emit iSupportReceived(d->params.join(" "));
217  break;
218  }
219 
221  {
222  d->params.takeFirst(); // Own nickname.
223  QString channel = d->params.takeFirst();
224  QString topic = joinAndTrimColonIfNecessary(d->params);
225  QString msg = tr("Topic: %1").arg(topic);
226  emit printWithClass(msg, channel, IRCMessageClass(IRCMessageClass::ChannelAction));
227  break;
228  }
229 
231  {
232  d->params.takeFirst(); // Own nickname.
233  QString channel = d->params.takeFirst();
234  QString who = d->params.takeFirst();
235  qint64 timestampSeconds = d->params.takeFirst().toLongLong();
236  QDateTime date = QDateTime::fromMSecsSinceEpoch(timestampSeconds * 1000);
237  QString msg = tr("Topic set by %1 on %2.").arg(who, date.toString("yyyy-MM-dd hh:mm:ss"));
238  emit printWithClass(msg, channel, IRCMessageClass(IRCMessageClass::ChannelAction));
239  break;
240  }
241 
243  {
244  d->params.takeFirst(); // Own nickname.
245  QString channel = d->params.takeFirst();
246  QString url = joinAndTrimColonIfNecessary(d->params);
247  emit printWithClass(tr("URL: %1").arg(url), channel, IRCMessageClass::ChannelAction);
248  break;
249  }
250 
252  {
253  d->params.takeFirst(); // Own nickname.
254  QString channel = d->params.takeFirst();
255  QString time = joinAndTrimColonIfNecessary(d->params);
256  emit printWithClass(tr("Created time: %1").arg(time), channel,
257  IRCMessageClass::ChannelAction);
258  break;
259  }
260 
262  {
263  // Namelists.
264 
265  // Attempt to extract the channel name. For some reason
266  // irc.skulltag.net returns a '=' character between the message type
267  // signature and the channel name. RFC 1459 doesn't say anything about
268  // such behavior, at least not in the chapter 6. We should protect
269  // ourselves from such unwelcome surprises.
270  QString channelName = "";
271  while (!IRCGlobal::isChannelName(channelName) && !d->params.isEmpty())
272  {
273  channelName = d->params.takeFirst();
274  }
275 
276  if (channelName.isEmpty())
277  {
278  emit parseError(tr("RPLNamReply: Received names list but no channel name."));
279  return IRCResponseParseResult();
280  }
281 
282  // Remaining values will be user names. Send them all as a strings list.
283  // Remember to remove the ":" character from the first name.
284  if (!d->params.isEmpty())
285  d->params[0] = d->params[0].remove(0, 1);
286 
287  emit namesListReceived(channelName, d->params);
288  break;
289  }
290 
292  {
293  QString channel = d->params[1];
294  emit namesListEndReceived(channel);
295  break;
296  }
297 
301  {
302  // First param is username, drop it.
303  d->params.takeFirst();
304 
305  if (enumType == IRCResponseType::RPLMOTDStart)
306  {
307  // We will print additional separator line if it's a start of
308  // the MOTD.
309  emit print("\n----------------------", QString());
310  }
311 
312  emit print(joinAndTrimColonIfNecessary(d->params), QString());
313 
314  if (enumType == IRCResponseType::RPLEndOfMOTD)
315  {
316  // Again, we will print additional separator line if it's the
317  // end of the MOTD.
318  emit print("----------------------\n", QString());
319  }
320 
321  break;
322  }
323 
324  // Extract correct message and print it.
327  {
328  // Drop the first param.
329  d->params.takeFirst();
330 
331  // Join and print the rest.
332  emit print(joinAndTrimColonIfNecessary(d->params), QString());
333  break;
334  }
335 
336  // Extract correct message and print it.
340  {
341  // Drop the first param.
342  d->params.takeFirst();
343 
344  // Here the first param is always an integer, and colon is located
345  // afterwards.
346  QString number = d->params.takeFirst();
347  emit print(number + " " + joinAndTrimColonIfNecessary(d->params), QString());
348  break;
349  }
350 
352  {
353  // Drop the first param.
354  d->params.takeFirst();
355 
356  // This is the real nickname.
357  QString nickname = d->params.takeFirst();
358 
359  emit noSuchNickname(nickname);
360  break;
361  }
362 
364  {
365  // Own nickname.
366  d->params.takeFirst();
367  QString badNick = d->params.takeFirst();
368  QString msg = tr("Erroneous nickname: %1").arg(badNick);
369  if (d->params.join(" ").compare(":Erroneous nickname", Qt::CaseInsensitive) != 0)
370  msg += tr(" (%1)").arg(joinAndTrimColonIfNecessary(d->params));
371  emit printToNetworksCurrentChatBox(msg, IRCMessageClass::Error);
372  break;
373  }
374 
376  {
377  // Drop the first param.
378  d->params.takeFirst();
379 
380  QString nickname = d->params.takeFirst();
381 
382  emit nicknameInUse(nickname);
383  break;
384  }
385 
386  case IRCResponseType::ERRChannelIsFull:
387  case IRCResponseType::ERRInviteOnlyChan:
388  case IRCResponseType::ERRBannedFromChan:
389  case IRCResponseType::ERRBadChannelKey:
390  case IRCResponseType::ERRBadChannelMask:
391  case IRCResponseType::ERRNoChanModes:
394  {
395  d->params.takeFirst(); // User
396  QString channel = d->params.takeFirst();
397  QString reason = joinAndTrimColonIfNecessary(d->params);
398  switch (enumType)
399  {
400  case IRCResponseType::ERRChannelIsFull:
401  case IRCResponseType::ERRInviteOnlyChan:
402  case IRCResponseType::ERRBannedFromChan:
403  case IRCResponseType::ERRBadChannelKey:
404  emit printToNetworksCurrentChatBox(tr("%1: %2").arg(channel, reason),
405  IRCMessageClass::Error);
406  break;
407  default:
408  emit printWithClass(reason, channel, IRCMessageClass::ChannelAction);
409  break;
410  }
411 
412  break;
413  }
414 
415  case IRCResponseType::Join:
416  {
417  QString channel = d->params[0];
418  trimColonIfNecessary(channel);
419 
420  emit userJoinsChannel(channel, d->sender, d->prefix);
421  break;
422  }
423 
424  case IRCResponseType::Kick:
425  {
426  QString channel = d->params.takeFirst();
427  QString whoIsKicked = d->params.takeFirst();
428 
429  QString reason = joinAndTrimColonIfNecessary(d->params);
430 
431  emit kick(channel, d->sender, whoIsKicked, reason);
432  break;
433  }
434 
435  case IRCResponseType::Kill:
436  {
437  QString victim = d->params.takeFirst();
438  QString comment = joinAndTrimColonIfNecessary(d->params);
439  emit kill(victim, comment);
440  break;
441  }
442 
443  case IRCResponseType::Mode:
444  {
445  QString channel = d->params.takeFirst();
446  QString flagsString = d->params.takeFirst();
447 
448  // If there are no more params left on the list then this modes
449  // are for the channel itself. Otherwise they are for the users.
450  if (!d->params.isEmpty())
451  {
452  emit modeInfo(channel, d->sender, flagsString + " " + d->params.join(" "));
453  parseUserModeMessage(channel, flagsString, d->params);
454  }
455 
456  break;
457  }
458 
459  case IRCResponseType::Nick:
460  {
461  QString oldNickname = d->sender;
462  QString newNickname = d->params[0];
463  trimColonIfNecessary(newNickname);
464 
465  emit userChangesNickname(oldNickname, newNickname);
466  break;
467  }
468 
469  case IRCResponseType::Part:
470  {
471  QString farewellMessage = QString();
472  QString channel = d->params[0];
473 
474  if (d->params.size() > 1)
475  {
476  d->params.pop_front();
477 
478  farewellMessage = joinAndTrimColonIfNecessary(d->params);
479  }
480 
481  emit userPartsChannel(channel, d->sender, farewellMessage);
482  break;
483  }
484 
485  case IRCResponseType::Ping:
486  {
487  QString pongToWhom = d->params[0];
488 
489  emit sendPongMessage(pongToWhom);
490  break;
491  }
492 
493  case IRCResponseType::PrivMsg:
494  case IRCResponseType::Notice:
495  parsePrivMsgOrNotice();
496  break;
497 
498  case IRCResponseType::Quit:
499  {
500  QString farewellMessage = QString();
501  farewellMessage = joinAndTrimColonIfNecessary(d->params);
502 
503  emit userQuitsNetwork(d->sender, farewellMessage);
504  break;
505  }
506 
507  case IRCResponseType::Topic:
508  {
509  QString channel = d->params.takeFirst();
510  QString topic = joinAndTrimColonIfNecessary(d->params);
511  QString msg = tr("New topic set by user %1:\n%2").arg(d->sender, topic);
512  emit printWithClass(msg, channel, IRCMessageClass(IRCMessageClass::ChannelAction));
513  break;
514  }
515 
517  {
518  // Messages below 100 are some generic server responses to connect
519  // event.
520  if (responseType.numericType() > 1)
521  {
522  emit print(joinAndTrimColonIfNecessary(d->params), "");
523  return IRCResponseParseResult(responseType, true);
524  }
525 
526  return IRCResponseParseResult(responseType, false);
527  }
528 
529  default:
530  emit parseError(tr(
531  "IRCResponseParser: Type '%1' was recognized but there has been no parse code implemented for it.\
532 (yep, it's a bug in the application!)"
533  ).arg(d->type));
534  return IRCResponseParseResult(responseType, true);
535  }
536 
537  return IRCResponseParseResult(responseType, true);
538 }
539 
540 void IRCResponseParser::parsePrivMsgOrNotice()
541 {
542  if (isPrefixIgnored())
543  return;
544  QString recipient = d->params.takeFirst();
545  if (!IRCGlobal::isChannelName(recipient))
546  {
547  // If recipient name is not the channel the
548  // "recipient" QString will point to this client's user.
549  // In order to get a proper recipient we need to use the
550  // "sender" QString instead.
551  recipient = d->sender;
552  }
553 
554  // Join the list to form message contents.
555  QString content = joinAndTrimColonIfNecessary(d->params);
556 
557  IRCResponseType responseType(d->type);
558  IRCCtcpParser::MessageType ctcpMsgType = (responseType == IRCResponseType::Notice) ?
560  IRCCtcpParser ctcp(d->network, d->sender, recipient, content, ctcpMsgType);
561  if (ctcp.parse())
562  {
563  switch (ctcp.echo())
564  {
565  case IRCCtcpParser::PrintAsNormalMessage:
566  emit privMsgLiteralReceived(recipient, ctcp.printable(), IRCMessageClass::Ctcp);
567  break;
568  case IRCCtcpParser::DisplayInServerTab:
569  emit printWithClass(ctcp.printable(), QString(), IRCMessageClass::Ctcp);
570  break;
571  case IRCCtcpParser::DisplayThroughGlobalMessage:
572  emit printToNetworksCurrentChatBox(ctcp.printable(), IRCMessageClass::Ctcp);
573  break;
574  case IRCCtcpParser::DontShow:
575  break;
576  default:
577  gLog << QString("Unhandled CTCP echo type: %1").arg(ctcp.echo());
578  assert(false && "Unhandled CTCP echo type");
579  break;
580  }
581  if (!ctcp.reply().isEmpty() && responseType.type() != IRCResponseType::Notice)
582  d->network->sendMessage(QString("/NOTICE %1 %2%3%2").arg(d->sender, QChar(0x1), ctcp.reply()));
583  }
584  else
585  {
586  if (responseType == IRCResponseType::PrivMsg)
587  emit privMsgReceived(recipient, d->sender, content);
588  else if (responseType == IRCResponseType::Notice)
589  emit print(tr("[%1]: %2").arg(d->sender, content), recipient);
590  else
591  emit parseError(tr("Type '%1' was incorrectly parsed in PrivMsg block.").arg(d->type));
592  }
593 }
594 
595 void IRCResponseParser::parseUserModeMessage(const QString &channel, QString flagsString, QStringList &nicknames)
596 {
597  // For each flag character there should be one nickname on the list.
598  // If there are less nicknames than characters we will simply abort.
599  // Of course add/subtract characters are not counted here.
600 
601  // The first character should always define the flagMode.
602  FlagModes flagMode = getFlagMode(flagsString[0].toLatin1());
603 
604  if (flagMode == FlagModeError)
605  {
606  emit parseError(tr("MODE flags string from IRC server are incorrect: \"%1\". "
607  "Information for channel \"%2\" might not be correct anymore.")
608  .arg(flagsString, channel));
609  return;
610  }
611 
612  for (int i = 1; i < flagsString.size(); ++i)
613  {
614  char flagChar = flagsString[i].toLatin1();
615 
616  FlagModes tmpFlagMode = getFlagMode(flagChar);
617  if (tmpFlagMode == FlagModeError)
618  {
619  if (nicknames.empty())
620  return;
621 
622  QList<char> addedFlags;
623  QList<char> removedFlags;
624 
625  QString name = nicknames[0];
626 
627  switch (flagMode)
628  {
629  case FlagModeAdd:
630  addedFlags << flagChar;
631  break;
632 
633  case FlagModeRemove:
634  removedFlags << flagChar;
635  break;
636 
637  default:
638  emit parseError(tr("IRCResponseParser::parseUserModeMessage(): "
639  "wrong FlagMode. Information for channel \"%2\" might not be correct anymore."));
640  return;
641  }
642 
643  emit userModeChanged(channel, name, addedFlags, removedFlags);
644  // Drop a name from the list and continue.
645  nicknames.pop_front();
646  }
647  else
648  {
649  flagMode = tmpFlagMode;
650  continue;
651  }
652  }
653 }
654 
655 QString &IRCResponseParser::trimColonIfNecessary(QString &str) const
656 {
657  if (!str.isEmpty() && str[0] == ':')
658  str.remove(0, 1);
659 
660  return str;
661 }