1 #include "IrcMessageHandler.hpp"
2 
3 #include "Application.hpp"
4 #include "common/QLogging.hpp"
5 #include "controllers/accounts/AccountController.hpp"
6 #include "messages/LimitedQueue.hpp"
7 #include "messages/Message.hpp"
8 #include "providers/twitch/TwitchAccountManager.hpp"
9 #include "providers/twitch/TwitchChannel.hpp"
10 #include "providers/twitch/TwitchHelpers.hpp"
11 #include "providers/twitch/TwitchIrcServer.hpp"
12 #include "providers/twitch/TwitchMessageBuilder.hpp"
13 #include "singletons/Resources.hpp"
14 #include "singletons/Settings.hpp"
15 #include "singletons/WindowManager.hpp"
16 #include "util/FormatTime.hpp"
17 #include "util/Helpers.hpp"
18 #include "util/IrcHelpers.hpp"
19 
20 #include <IrcMessage>
21 
22 #include <unordered_set>
23 
24 namespace {
25 using namespace chatterino;
26 
27 // Message types below are the ones that might contain special user's message on USERNOTICE
28 static const QSet<QString> specialMessageTypes{
29     "sub",            //
30     "subgift",        //
31     "resub",          // resub messages
32     "bitsbadgetier",  // bits badge upgrade
33     "ritual",         // new viewer ritual
34 };
35 
generateBannedMessage(bool confirmedBan)36 MessagePtr generateBannedMessage(bool confirmedBan)
37 {
38     const auto linkColor = MessageColor(MessageColor::Link);
39     const auto accountsLink = Link(Link::Reconnect, QString());
40     const auto bannedText =
41         confirmedBan
42             ? QString("You were banned from this channel!")
43             : QString(
44                   "Your connection to this channel was unexpectedly dropped.");
45 
46     const auto reconnectPromptText =
47         confirmedBan
48             ? QString(
49                   "If you believe you have been unbanned, try reconnecting.")
50             : QString("Try reconnecting.");
51 
52     MessageBuilder builder;
53     auto text = QString("%1 %2").arg(bannedText, reconnectPromptText);
54     builder.message().messageText = text;
55     builder.message().searchText = text;
56     builder.message().flags.set(MessageFlag::System);
57 
58     builder.emplace<TimestampElement>();
59     builder.emplace<TextElement>(bannedText, MessageElementFlag::Text,
60                                  MessageColor::System);
61     builder
62         .emplace<TextElement>(reconnectPromptText, MessageElementFlag::Text,
63                               linkColor)
64         ->setLink(accountsLink);
65 
66     return builder.release();
67 }
68 
69 }  // namespace
70 namespace chatterino {
71 
relativeSimilarity(const QString & str1,const QString & str2)72 static float relativeSimilarity(const QString &str1, const QString &str2)
73 {
74     // Longest Common Substring Problem
75     std::vector<std::vector<int>> tree(str1.size(),
76                                        std::vector<int>(str2.size(), 0));
77     int z = 0;
78 
79     for (int i = 0; i < str1.size(); ++i)
80     {
81         for (int j = 0; j < str2.size(); ++j)
82         {
83             if (str1[i] == str2[j])
84             {
85                 if (i == 0 || j == 0)
86                 {
87                     tree[i][j] = 1;
88                 }
89                 else
90                 {
91                     tree[i][j] = tree[i - 1][j - 1] + 1;
92                 }
93                 if (tree[i][j] > z)
94                 {
95                     z = tree[i][j];
96                 }
97             }
98             else
99             {
100                 tree[i][j] = 0;
101             }
102         }
103     }
104 
105     // ensure that no div by 0
106     return z == 0 ? 0.f
107                   : float(z) /
108                         std::max<int>(1, std::max(str1.size(), str2.size()));
109 };
110 
similarity(MessagePtr msg,const LimitedQueueSnapshot<MessagePtr> & messages)111 float IrcMessageHandler::similarity(
112     MessagePtr msg, const LimitedQueueSnapshot<MessagePtr> &messages)
113 {
114     float similarityPercent = 0.0f;
115     int bySameUser = 0;
116     for (int i = 1; bySameUser < getSettings()->hideSimilarMaxMessagesToCheck;
117          ++i)
118     {
119         if (messages.size() < i)
120         {
121             break;
122         }
123         const auto &prevMsg = messages[messages.size() - i];
124         if (prevMsg->parseTime.secsTo(QTime::currentTime()) >=
125             getSettings()->hideSimilarMaxDelay)
126         {
127             break;
128         }
129         if (msg->loginName != prevMsg->loginName)
130         {
131             continue;
132         }
133         ++bySameUser;
134         similarityPercent = std::max(
135             similarityPercent,
136             relativeSimilarity(msg->messageText, prevMsg->messageText));
137     }
138     return similarityPercent;
139 }
140 
setSimilarityFlags(MessagePtr msg,ChannelPtr chan)141 void IrcMessageHandler::setSimilarityFlags(MessagePtr msg, ChannelPtr chan)
142 {
143     if (getSettings()->similarityEnabled)
144     {
145         bool isMyself = msg->loginName ==
146                         getApp()->accounts->twitch.getCurrent()->getUserName();
147         bool hideMyself = getSettings()->hideSimilarMyself;
148 
149         if (isMyself && !hideMyself)
150         {
151             return;
152         }
153 
154         if (IrcMessageHandler::similarity(msg, chan->getMessageSnapshot()) >
155             getSettings()->similarityPercentage)
156         {
157             msg->flags.set(MessageFlag::Similar, true);
158             if (getSettings()->colorSimilarDisabled)
159             {
160                 msg->flags.set(MessageFlag::Disabled, true);
161             }
162         }
163     }
164 }
165 
parseBadges(QString badgesString)166 static QMap<QString, QString> parseBadges(QString badgesString)
167 {
168     QMap<QString, QString> badges;
169 
170     for (const auto &badgeData : badgesString.split(','))
171     {
172         auto parts = badgeData.split('/');
173         if (parts.length() != 2)
174         {
175             continue;
176         }
177 
178         badges.insert(parts[0], parts[1]);
179     }
180 
181     return badges;
182 }
183 
instance()184 IrcMessageHandler &IrcMessageHandler::instance()
185 {
186     static IrcMessageHandler instance;
187     return instance;
188 }
189 
parseMessage(Channel * channel,Communi::IrcMessage * message)190 std::vector<MessagePtr> IrcMessageHandler::parseMessage(
191     Channel *channel, Communi::IrcMessage *message)
192 {
193     std::vector<MessagePtr> builtMessages;
194 
195     auto command = message->command();
196 
197     if (command == "PRIVMSG")
198     {
199         return this->parsePrivMessage(
200             channel, static_cast<Communi::IrcPrivateMessage *>(message));
201     }
202     else if (command == "USERNOTICE")
203     {
204         return this->parseUserNoticeMessage(channel, message);
205     }
206     else if (command == "NOTICE")
207     {
208         return this->parseNoticeMessage(
209             static_cast<Communi::IrcNoticeMessage *>(message));
210     }
211 
212     return builtMessages;
213 }
214 
parsePrivMessage(Channel * channel,Communi::IrcPrivateMessage * message)215 std::vector<MessagePtr> IrcMessageHandler::parsePrivMessage(
216     Channel *channel, Communi::IrcPrivateMessage *message)
217 {
218     std::vector<MessagePtr> builtMessages;
219     MessageParseArgs args;
220     TwitchMessageBuilder builder(channel, message, args, message->content(),
221                                  message->isAction());
222     if (!builder.isIgnored())
223     {
224         builtMessages.emplace_back(builder.build());
225         builder.triggerHighlights();
226     }
227     return builtMessages;
228 }
229 
handlePrivMessage(Communi::IrcPrivateMessage * message,TwitchIrcServer & server)230 void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message,
231                                           TwitchIrcServer &server)
232 {
233     this->addMessage(message, message->target(), message->content(), server,
234                      false, message->isAction());
235 }
236 
addMessage(Communi::IrcMessage * _message,const QString & target,const QString & content,TwitchIrcServer & server,bool isSub,bool isAction)237 void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
238                                    const QString &target,
239                                    const QString &content,
240                                    TwitchIrcServer &server, bool isSub,
241                                    bool isAction)
242 {
243     QString channelName;
244     if (!trimChannelName(target, channelName))
245     {
246         return;
247     }
248 
249     auto chan = server.getChannelOrEmpty(channelName);
250 
251     if (chan->isEmpty())
252     {
253         return;
254     }
255 
256     MessageParseArgs args;
257     if (isSub)
258     {
259         args.trimSubscriberUsername = true;
260     }
261 
262     if (chan->isBroadcaster())
263     {
264         args.isStaffOrBroadcaster = true;
265     }
266 
267     auto channel = dynamic_cast<TwitchChannel *>(chan.get());
268 
269     const auto &tags = _message->tags();
270     if (const auto &it = tags.find("custom-reward-id"); it != tags.end())
271     {
272         const auto rewardId = it.value().toString();
273         if (!channel->isChannelPointRewardKnown(rewardId))
274         {
275             // Need to wait for pubsub reward notification
276             auto clone = _message->clone();
277             channel->channelPointRewardAdded.connect(
278                 [=, &server](ChannelPointReward reward) {
279                     if (reward.id == rewardId)
280                     {
281                         this->addMessage(clone, target, content, server, isSub,
282                                          isAction);
283                         clone->deleteLater();
284                         return true;
285                     }
286                     return false;
287                 });
288             return;
289         }
290         args.channelPointRewardId = rewardId;
291     }
292 
293     TwitchMessageBuilder builder(chan.get(), _message, args, content, isAction);
294 
295     if (isSub || !builder.isIgnored())
296     {
297         if (isSub)
298         {
299             builder->flags.set(MessageFlag::Subscription);
300             builder->flags.unset(MessageFlag::Highlighted);
301         }
302         auto msg = builder.build();
303 
304         IrcMessageHandler::setSimilarityFlags(msg, chan);
305 
306         if (!msg->flags.has(MessageFlag::Similar) ||
307             (!getSettings()->hideSimilar &&
308              getSettings()->shownSimilarTriggerHighlights))
309         {
310             builder.triggerHighlights();
311         }
312 
313         const auto highlighted = msg->flags.has(MessageFlag::Highlighted);
314         const auto showInMentions = msg->flags.has(MessageFlag::ShowInMentions);
315 
316         if (!isSub)
317         {
318             if (highlighted && showInMentions)
319             {
320                 server.mentionsChannel->addMessage(msg);
321             }
322         }
323 
324         chan->addMessage(msg);
325         if (auto chatters = dynamic_cast<ChannelChatters *>(chan.get()))
326         {
327             chatters->addRecentChatter(msg->displayName);
328         }
329     }
330 }
331 
handleRoomStateMessage(Communi::IrcMessage * message)332 void IrcMessageHandler::handleRoomStateMessage(Communi::IrcMessage *message)
333 {
334     const auto &tags = message->tags();
335 
336     // get twitch channel
337     QString chanName;
338     if (!trimChannelName(message->parameter(0), chanName))
339     {
340         return;
341     }
342     auto chan = getApp()->twitch.server->getChannelOrEmpty(chanName);
343 
344     auto *twitchChannel = dynamic_cast<TwitchChannel *>(chan.get());
345     if (!twitchChannel)
346     {
347         return;
348     }
349 
350     // room-id
351 
352     if (auto it = tags.find("room-id"); it != tags.end())
353     {
354         auto roomId = it.value().toString();
355         twitchChannel->setRoomId(roomId);
356     }
357 
358     // Room modes
359     {
360         auto roomModes = *twitchChannel->accessRoomModes();
361 
362         if (auto it = tags.find("emote-only"); it != tags.end())
363         {
364             roomModes.emoteOnly = it.value() == "1";
365         }
366         if (auto it = tags.find("subs-only"); it != tags.end())
367         {
368             roomModes.submode = it.value() == "1";
369         }
370         if (auto it = tags.find("slow"); it != tags.end())
371         {
372             roomModes.slowMode = it.value().toInt();
373         }
374         if (auto it = tags.find("r9k"); it != tags.end())
375         {
376             roomModes.r9k = it.value() == "1";
377         }
378         if (auto it = tags.find("followers-only"); it != tags.end())
379         {
380             roomModes.followerOnly = it.value().toInt();
381         }
382         twitchChannel->setRoomModes(roomModes);
383     }
384 
385     twitchChannel->roomModesChanged.invoke();
386 }
387 
handleClearChatMessage(Communi::IrcMessage * message)388 void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message)
389 {
390     // check parameter count
391     if (message->parameters().length() < 1)
392     {
393         return;
394     }
395 
396     QString chanName;
397     if (!trimChannelName(message->parameter(0), chanName))
398     {
399         return;
400     }
401 
402     // get channel
403     auto chan = getApp()->twitch.server->getChannelOrEmpty(chanName);
404 
405     if (chan->isEmpty())
406     {
407         qCDebug(chatterinoTwitch)
408             << "[IrcMessageHandler:handleClearChatMessage] Twitch channel"
409             << chanName << "not found";
410         return;
411     }
412 
413     // check if the chat has been cleared by a moderator
414     if (message->parameters().length() == 1)
415     {
416         chan->disableAllMessages();
417         chan->addMessage(
418             makeSystemMessage("Chat has been cleared by a moderator.",
419                               calculateMessageTimestamp(message)));
420 
421         return;
422     }
423 
424     // get username, duration and message of the timed out user
425     QString username = message->parameter(1);
426     QString durationInSeconds;
427     QVariant v = message->tag("ban-duration");
428     if (v.isValid())
429     {
430         durationInSeconds = v.toString();
431     }
432 
433     auto timeoutMsg =
434         MessageBuilder(timeoutMessage, username, durationInSeconds, false,
435                        calculateMessageTimestamp(message))
436             .release();
437     chan->addOrReplaceTimeout(timeoutMsg);
438 
439     // refresh all
440     getApp()->windows->repaintVisibleChatWidgets(chan.get());
441     if (getSettings()->hideModerated)
442     {
443         getApp()->windows->forceLayoutChannelViews();
444     }
445 }
446 
handleClearMessageMessage(Communi::IrcMessage * message)447 void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message)
448 {
449     // check parameter count
450     if (message->parameters().length() < 1)
451     {
452         return;
453     }
454 
455     QString chanName;
456     if (!trimChannelName(message->parameter(0), chanName))
457     {
458         return;
459     }
460 
461     // get channel
462     auto chan = getApp()->twitch.server->getChannelOrEmpty(chanName);
463 
464     if (chan->isEmpty())
465     {
466         qCDebug(chatterinoTwitch)
467             << "[IrcMessageHandler:handleClearMessageMessage] Twitch "
468                "channel"
469             << chanName << "not found";
470         return;
471     }
472 
473     auto tags = message->tags();
474 
475     QString targetID = tags.value("target-msg-id").toString();
476 
477     auto msg = chan->findMessage(targetID);
478     if (msg == nullptr)
479         return;
480 
481     msg->flags.set(MessageFlag::Disabled);
482     if (!getSettings()->hideDeletionActions)
483     {
484         MessageBuilder builder;
485         TwitchMessageBuilder::deletionMessage(msg, &builder);
486         chan->addMessage(builder.release());
487     }
488 }
489 
handleUserStateMessage(Communi::IrcMessage * message)490 void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
491 {
492     auto currentUser = getApp()->accounts->twitch.getCurrent();
493 
494     // set received emote-sets, used in TwitchAccount::loadUserstateEmotes
495     bool emoteSetsChanged = currentUser->setUserstateEmoteSets(
496         message->tag("emote-sets").toString().split(","));
497 
498     if (emoteSetsChanged)
499     {
500         currentUser->loadUserstateEmotes([] {});
501     }
502 
503     QString channelName;
504     if (!trimChannelName(message->parameter(0), channelName))
505     {
506         return;
507     }
508 
509     auto c = getApp()->twitch.server->getChannelOrEmpty(channelName);
510     if (c->isEmpty())
511     {
512         return;
513     }
514 
515     // Checking if currentUser is a VIP or staff member
516     QVariant _badges = message->tag("badges");
517     if (_badges.isValid())
518     {
519         TwitchChannel *tc = dynamic_cast<TwitchChannel *>(c.get());
520         if (tc != nullptr)
521         {
522             auto parsedBadges = parseBadges(_badges.toString());
523             tc->setVIP(parsedBadges.contains("vip"));
524             tc->setStaff(parsedBadges.contains("staff"));
525         }
526     }
527 
528     // Checking if currentUser is a moderator
529     QVariant _mod = message->tag("mod");
530     if (_mod.isValid())
531     {
532         TwitchChannel *tc = dynamic_cast<TwitchChannel *>(c.get());
533         if (tc != nullptr)
534         {
535             tc->setMod(_mod == "1");
536         }
537     }
538 }
539 
540 // This will emit only once and right after user logs in to IRC - reset emote data and reload emotes
handleGlobalUserStateMessage(Communi::IrcMessage * message)541 void IrcMessageHandler::handleGlobalUserStateMessage(
542     Communi::IrcMessage *message)
543 {
544     auto currentUser = getApp()->accounts->twitch.getCurrent();
545 
546     // set received emote-sets, this time used to initially load emotes
547     // NOTE: this should always return true unless we reconnect
548     auto emoteSetsChanged = currentUser->setUserstateEmoteSets(
549         message->tag("emote-sets").toString().split(","));
550 
551     // We should always attempt to reload emotes even on reconnections where
552     // emoteSetsChanged, since we want to trigger emote reloads when
553     // "currentUserChanged" signal is emitted
554     qCDebug(chatterinoTwitch) << emoteSetsChanged << message->toData();
555     currentUser->loadEmotes();
556 }
557 
handleWhisperMessage(Communi::IrcMessage * message)558 void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
559 {
560     MessageParseArgs args;
561 
562     args.isReceivedWhisper = true;
563 
564     auto c = getApp()->twitch.server->whispersChannel.get();
565 
566     TwitchMessageBuilder builder(c, message, args, message->parameter(1),
567                                  false);
568 
569     if (builder.isIgnored())
570     {
571         return;
572     }
573 
574     builder->flags.set(MessageFlag::Whisper);
575     MessagePtr _message = builder.build();
576     builder.triggerHighlights();
577 
578     getApp()->twitch.server->lastUserThatWhisperedMe.set(builder.userName);
579 
580     if (_message->flags.has(MessageFlag::Highlighted))
581     {
582         getApp()->twitch.server->mentionsChannel->addMessage(_message);
583     }
584 
585     c->addMessage(_message);
586 
587     auto overrideFlags = boost::optional<MessageFlags>(_message->flags);
588     overrideFlags->set(MessageFlag::DoNotTriggerNotification);
589     overrideFlags->set(MessageFlag::DoNotLog);
590 
591     if (getSettings()->inlineWhispers)
592     {
593         getApp()->twitch.server->forEachChannel(
594             [&_message, overrideFlags](ChannelPtr channel) {
595                 channel->addMessage(_message, overrideFlags);
596             });
597     }
598 }
599 
parseUserNoticeMessage(Channel * channel,Communi::IrcMessage * message)600 std::vector<MessagePtr> IrcMessageHandler::parseUserNoticeMessage(
601     Channel *channel, Communi::IrcMessage *message)
602 {
603     std::vector<MessagePtr> builtMessages;
604 
605     auto tags = message->tags();
606     auto parameters = message->parameters();
607 
608     QString msgType = tags.value("msg-id").toString();
609     QString content;
610     if (parameters.size() >= 2)
611     {
612         content = parameters[1];
613     }
614 
615     if (specialMessageTypes.contains(msgType))
616     {
617         // Messages are not required, so they might be empty
618         if (!content.isEmpty())
619         {
620             MessageParseArgs args;
621             args.trimSubscriberUsername = true;
622 
623             TwitchMessageBuilder builder(channel, message, args, content,
624                                          false);
625             builder->flags.set(MessageFlag::Subscription);
626             builder->flags.unset(MessageFlag::Highlighted);
627             builtMessages.emplace_back(builder.build());
628         }
629     }
630 
631     auto it = tags.find("system-msg");
632 
633     if (it != tags.end())
634     {
635         // By default, we return value of system-msg tag
636         QString messageText = it.value().toString();
637 
638         if (msgType == "bitsbadgetier")
639         {
640             messageText =
641                 QString("%1 just earned a new %2 Bits badge!")
642                     .arg(tags.value("display-name").toString(),
643                          kFormatNumbers(
644                              tags.value("msg-param-threshold").toInt()));
645         }
646 
647         auto b = MessageBuilder(systemMessage, parseTagString(messageText),
648                                 calculateMessageTimestamp(message));
649 
650         b->flags.set(MessageFlag::Subscription);
651         auto newMessage = b.release();
652         builtMessages.emplace_back(newMessage);
653     }
654 
655     return builtMessages;
656 }
657 
handleUserNoticeMessage(Communi::IrcMessage * message,TwitchIrcServer & server)658 void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message,
659                                                 TwitchIrcServer &server)
660 {
661     auto tags = message->tags();
662     auto parameters = message->parameters();
663 
664     auto target = parameters[0];
665     QString msgType = tags.value("msg-id").toString();
666     QString content;
667     if (parameters.size() >= 2)
668     {
669         content = parameters[1];
670     }
671 
672     if (specialMessageTypes.contains(msgType))
673     {
674         // Messages are not required, so they might be empty
675         if (!content.isEmpty())
676         {
677             this->addMessage(message, target, content, server, true, false);
678         }
679     }
680 
681     auto it = tags.find("system-msg");
682 
683     if (it != tags.end())
684     {
685         // By default, we return value of system-msg tag
686         QString messageText = it.value().toString();
687 
688         if (msgType == "bitsbadgetier")
689         {
690             messageText =
691                 QString("%1 just earned a new %2 Bits badge!")
692                     .arg(tags.value("display-name").toString(),
693                          kFormatNumbers(
694                              tags.value("msg-param-threshold").toInt()));
695         }
696 
697         auto b = MessageBuilder(systemMessage, parseTagString(messageText),
698                                 calculateMessageTimestamp(message));
699 
700         b->flags.set(MessageFlag::Subscription);
701         auto newMessage = b.release();
702 
703         QString channelName;
704 
705         if (message->parameters().size() < 1)
706         {
707             return;
708         }
709 
710         if (!trimChannelName(message->parameter(0), channelName))
711         {
712             return;
713         }
714 
715         auto chan = server.getChannelOrEmpty(channelName);
716 
717         if (!chan->isEmpty())
718         {
719             chan->addMessage(newMessage);
720         }
721     }
722 }
723 
parseNoticeMessage(Communi::IrcNoticeMessage * message)724 std::vector<MessagePtr> IrcMessageHandler::parseNoticeMessage(
725     Communi::IrcNoticeMessage *message)
726 {
727     if (message->content().startsWith("Login auth", Qt::CaseInsensitive))
728     {
729         const auto linkColor = MessageColor(MessageColor::Link);
730         const auto accountsLink = Link(Link::OpenAccountsPage, QString());
731         const auto curUser = getApp()->accounts->twitch.getCurrent();
732         const auto expirationText = QString("Login expired for user \"%1\"!")
733                                         .arg(curUser->getUserName());
734         const auto loginPromptText = QString("Try adding your account again.");
735 
736         MessageBuilder builder;
737         auto text = QString("%1 %2").arg(expirationText, loginPromptText);
738         builder.message().messageText = text;
739         builder.message().searchText = text;
740         builder.message().flags.set(MessageFlag::System);
741         builder.message().flags.set(MessageFlag::DoNotTriggerNotification);
742 
743         builder.emplace<TimestampElement>();
744         builder.emplace<TextElement>(expirationText, MessageElementFlag::Text,
745                                      MessageColor::System);
746         builder
747             .emplace<TextElement>(loginPromptText, MessageElementFlag::Text,
748                                   linkColor)
749             ->setLink(accountsLink);
750 
751         return {builder.release()};
752     }
753     else if (message->content().startsWith("You are permanently banned "))
754     {
755         return {generateBannedMessage(true)};
756     }
757     else if (message->tags().value("msg-id") == "msg_timedout")
758     {
759         std::vector<MessagePtr> builtMessage;
760 
761         QString remainingTime =
762             formatTime(message->content().split(" ").value(5));
763         QString formattedMessage =
764             QString("You are timed out for %1.")
765                 .arg(remainingTime.isEmpty() ? "0s" : remainingTime);
766 
767         builtMessage.emplace_back(makeSystemMessage(
768             formattedMessage, calculateMessageTimestamp(message)));
769 
770         return builtMessage;
771     }
772 
773     // default case
774     std::vector<MessagePtr> builtMessages;
775 
776     builtMessages.emplace_back(makeSystemMessage(
777         message->content(), calculateMessageTimestamp(message)));
778 
779     return builtMessages;
780 }
781 
handleNoticeMessage(Communi::IrcNoticeMessage * message)782 void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message)
783 {
784     auto builtMessages = this->parseNoticeMessage(message);
785 
786     for (const auto &msg : builtMessages)
787     {
788         QString channelName;
789         if (!trimChannelName(message->target(), channelName) ||
790             channelName == "jtv")
791         {
792             // Notice wasn't targeted at a single channel, send to all twitch
793             // channels
794             getApp()->twitch.server->forEachChannelAndSpecialChannels(
795                 [msg](const auto &c) {
796                     c->addMessage(msg);
797                 });
798 
799             return;
800         }
801 
802         auto channel = getApp()->twitch.server->getChannelOrEmpty(channelName);
803 
804         if (channel->isEmpty())
805         {
806             qCDebug(chatterinoTwitch)
807                 << "[IrcManager:handleNoticeMessage] Channel" << channelName
808                 << "not found in channel manager";
809             return;
810         }
811 
812         QString tags = message->tags().value("msg-id").toString();
813         if (tags == "bad_delete_message_error" || tags == "usage_delete")
814         {
815             channel->addMessage(makeSystemMessage(
816                 "Usage: \"/delete <msg-id>\" - can't take more "
817                 "than one argument"));
818         }
819         else if (tags == "host_on" || tags == "host_target_went_offline")
820         {
821             bool hostOn = (tags == "host_on");
822             QStringList parts = msg->messageText.split(QLatin1Char(' '));
823             if ((hostOn && parts.size() != 3) || (!hostOn && parts.size() != 7))
824             {
825                 return;
826             }
827             auto &channelName = hostOn ? parts[2] : parts[0];
828             if (channelName.size() < 2)
829             {
830                 return;
831             }
832             if (hostOn)
833             {
834                 channelName.chop(1);
835             }
836             MessageBuilder builder;
837             TwitchMessageBuilder::hostingSystemMessage(channelName, &builder,
838                                                        hostOn);
839             channel->addMessage(builder.release());
840         }
841         else
842         {
843             channel->addMessage(msg);
844         }
845     }
846 }
847 
handleJoinMessage(Communi::IrcMessage * message)848 void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message)
849 {
850     auto channel = getApp()->twitch.server->getChannelOrEmpty(
851         message->parameter(0).remove(0, 1));
852 
853     auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
854     if (!twitchChannel)
855     {
856         return;
857     }
858 
859     if (message->nick() !=
860             getApp()->accounts->twitch.getCurrent()->getUserName() &&
861         getSettings()->showJoins.getValue())
862     {
863         twitchChannel->addJoinedUser(message->nick());
864     }
865 }
866 
handlePartMessage(Communi::IrcMessage * message)867 void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message)
868 {
869     auto channel = getApp()->twitch.server->getChannelOrEmpty(
870         message->parameter(0).remove(0, 1));
871 
872     auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
873     if (!twitchChannel)
874     {
875         return;
876     }
877 
878     const auto selfAccountName =
879         getApp()->accounts->twitch.getCurrent()->getUserName();
880     if (message->nick() != selfAccountName &&
881         getSettings()->showParts.getValue())
882     {
883         twitchChannel->addPartedUser(message->nick());
884     }
885 
886     if (message->nick() == selfAccountName)
887     {
888         channel->addMessage(generateBannedMessage(false));
889     }
890 }
891 }  // namespace chatterino
892