From f04e7e54e41701a9bb4502993e75a45180a40cd0 Mon Sep 17 00:00:00 2001 From: nerix Date: Wed, 9 Oct 2024 11:27:16 +0200 Subject: [PATCH 01/40] fix: only unpause if a selected page exists (#5637) * fix: only unpause if a selected page exists * chore: add changelog entry * nit: reduce changes --------- Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 2 +- src/widgets/Window.cpp | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4976ddfaa..9b0d1e7fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ - Bugfix: Fixed tooltips and input completion popups not working after moving a split. (#5541, #5576) - Bugfix: Fixed rare issue on shutdown where the client would hang. (#5557) - Bugfix: Fixed `/clearmessages` not working with more than one window. (#5489) -- Bugfix: Fixed splits staying paused after unfocusing Chatterino in certain configurations. (#5504) +- Bugfix: Fixed splits staying paused after unfocusing Chatterino in certain configurations. (#5504, #5637) - Bugfix: Links with invalid characters in the domain are no longer detected. (#5509) - Bugfix: Fixed janky selection for messages with RTL segments (selection is still wrong, but consistently wrong). (#5525) - Bugfix: Fixed event emotes not showing up in autocomplete and popups. (#5239, #5580, #5582) diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index d91ca41df..c095c9c28 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -113,20 +113,14 @@ bool Window::event(QEvent *event) } case QEvent::WindowDeactivate: { - for (const auto &split : - this->notebook_->getSelectedPage()->getSplits()) - { - split->unpause(); - } - auto *page = this->notebook_->getSelectedPage(); if (page != nullptr) { std::vector splits = page->getSplits(); - for (Split *split : splits) { + split->unpause(); split->updateLastReadMessage(); } From 9f196c67eaa7685f5feb317845393bd30cde3d04 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Thu, 10 Oct 2024 02:17:29 +0200 Subject: [PATCH 02/40] ci: add `skip-clang-tidy` label (#5639) --- .github/workflows/clang-tidy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index 44f9da5c1..d382e972c 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -10,6 +10,7 @@ concurrency: jobs: review: + if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-clang-tidy') }} name: "clang-tidy ${{ matrix.os }}, Qt ${{ matrix.qt-version }})" runs-on: ${{ matrix.os }} strategy: From bdc12ffb3f4bc582cb8abd52a8c12629dc444ca2 Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Sat, 12 Oct 2024 04:40:33 -0500 Subject: [PATCH 03/40] feat: indicate multi-month subs and resubs (#5642) --- CHANGELOG.md | 1 + src/providers/twitch/IrcMessageHandler.cpp | 62 ++++++++++++++++++++++ src/util/SampleData.cpp | 6 +++ 3 files changed, 69 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b0d1e7fb..dfe4dd2c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - Minor: Moderators can now see which mods start and cancel raids. (#5563) - Minor: The emote popup now reloads when Twitch emotes are reloaded. (#5580) - Minor: Added `--login ` CLI argument to specify which account to start logged in as. (#5626) +- Minor: Indicate when subscriptions and resubscriptions are for multiple months. (#5642) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426, #5612) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed restricted users usernames not being clickable. (#5405) diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 5dea75d21..7b093881f 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -566,6 +566,37 @@ std::vector parseUserNoticeMessage(Channel *channel, } } } + else if (msgType == "sub" || msgType == "resub") + { + if (auto tenure = tags.find("msg-param-multimonth-tenure"); + tenure != tags.end() && tenure.value().toInt() == 0) + { + int months = + tags.value("msg-param-multimonth-duration").toInt(); + if (months > 1) + { + int tier = tags.value("msg-param-sub-plan").toInt() / 1000; + messageText = + QString( + "%1 subscribed at Tier %2 for %3 months in advance") + .arg(tags.value("display-name").toString(), + QString::number(tier), + QString::number(months)); + if (msgType == "resub") + { + int cumulative = + tags.value("msg-param-cumulative-months").toInt(); + messageText += + QString(", reaching %1 months cumulatively so far!") + .arg(QString::number(cumulative)); + } + else + { + messageText += "!"; + } + } + } + } auto b = MessageBuilder(systemMessage, parseTagString(messageText), calculateMessageTime(message).time()); @@ -1084,6 +1115,37 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, } } } + else if (msgType == "sub" || msgType == "resub") + { + if (auto tenure = tags.find("msg-param-multimonth-tenure"); + tenure != tags.end() && tenure.value().toInt() == 0) + { + int months = + tags.value("msg-param-multimonth-duration").toInt(); + if (months > 1) + { + int tier = tags.value("msg-param-sub-plan").toInt() / 1000; + messageText = + QString( + "%1 subscribed at Tier %2 for %3 months in advance") + .arg(tags.value("display-name").toString(), + QString::number(tier), + QString::number(months)); + if (msgType == "resub") + { + int cumulative = + tags.value("msg-param-cumulative-months").toInt(); + messageText += + QString(", reaching %1 months cumulatively so far!") + .arg(QString::number(cumulative)); + } + else + { + messageText += "!"; + } + } + } + } auto b = MessageBuilder(systemMessage, parseTagString(messageText), calculateMessageTime(message).time()); diff --git a/src/util/SampleData.cpp b/src/util/SampleData.cpp index bc26fee0b..a31850d12 100644 --- a/src/util/SampleData.cpp +++ b/src/util/SampleData.cpp @@ -81,6 +81,12 @@ const QStringList &getSampleSubMessages() // multi-month sub gift by broadcaster R"(@user-id=35759863;msg-param-origin-id=2862055070165643340;display-name=Lucidfoxx;id=eeb3cdb8-337c-413a-9521-3a884ff78754;msg-param-gift-months=12;msg-param-sub-plan=1000;vip=0;emotes=;badges=broadcaster/1,subscriber/3042,partner/1;msg-param-recipient-user-name=ogprodigy;msg-param-recipient-id=53888434;badge-info=subscriber/71;room-id=35759863;msg-param-recipient-display-name=OGprodigy;msg-param-sub-plan-name=Silver\sPackage;subscriber=1;system-msg=Lucidfoxx\sgifted\sa\sTier\s1\ssub\sto\sOGprodigy!;login=lucidfoxx;msg-param-sender-count=0;user-type=;mod=0;flags=;rm-received-ts=1712803947891;color=#EB078D;msg-param-months=15;tmi-sent-ts=1712803947773;msg-id=subgift :tmi.twitch.tv USERNOTICE #pajlada)", + // multi-month sub + R"(@badge-info=subscriber/1;badges=subscriber/0;emotes=;msg-param-sub-plan=1000;msg-param-months=0;mod=0;login=foly__;room-id=11148817;flags=;user-id=441166175;msg-param-multimonth-tenure=0;msg-param-should-share-streak=0;system-msg=foly__\ssubscribed\sat\sTier\s1.;msg-id=sub;display-name=foly__;msg-param-sub-plan-name=Channel\sSubscription\s(k4sen);user-type=;id=327249bb-81bc-4f87-8b43-c05720a2dd64;msg-param-was-gifted=false;tmi-sent-ts=1728710962985;msg-param-cumulative-months=1;vip=0;color=;subscriber=1;msg-param-multimonth-duration=6 :tmi.twitch.tv USERNOTICE #pajlada)", + + // multi-month resub at tier 3 + R"(@system-msg=calm__like_a_tom\ssubscribed\sat\sTier\s3.\sThey've\ssubscribed\sfor\s9\smonths!;msg-param-cumulative-months=9;mod=0;msg-param-should-share-streak=0;msg-param-sub-plan-name=Executive\sProducer;color=#0000FF;msg-param-months=0;msg-param-multimonth-duration=6;user-type=;flags=;msg-id=resub;user-id=609242230;room-id=11148817;msg-param-multimonth-tenure=0;msg-param-sub-plan=3000;emotes=;badge-info=subscriber/9;msg-param-was-gifted=false;id=4a6e270c-8cdb-46e9-b602-f8177a79d472;badges=subscriber/3009;display-name=calm__like_a_tom;tmi-sent-ts=1725938011176;login=calm__like_a_tom;vip=0;subscriber=1 :tmi.twitch.tv USERNOTICE #pajlada)", + // first time sub R"(@badges=subscriber/0,premium/1;color=#0000FF;display-name=byebyeheart;emotes=;id=fe390424-ab89-4c33-bb5a-53c6e5214b9f;login=byebyeheart;mod=0;msg-id=sub;msg-param-months=0;msg-param-sub-plan-name=Dakotaz;msg-param-sub-plan=Prime;room-id=39298218;subscriber=0;system-msg=byebyeheart\sjust\ssubscribed\swith\sTwitch\sPrime!;tmi-sent-ts=1528190963670;turbo=0;user-id=131956000;user-type= :tmi.twitch.tv USERNOTICE #pajlada)", From bc1850ce2d6c31affbbdcfb6cb29b12fbf032d48 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 12 Oct 2024 12:08:30 +0200 Subject: [PATCH 04/40] fix: copy filters to overlay window (#5643) --- CHANGELOG.md | 2 +- src/widgets/OverlayWindow.cpp | 4 +++- src/widgets/OverlayWindow.hpp | 2 +- src/widgets/splits/Split.cpp | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe4dd2c9..82f4a511c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - Major: Add option to show pronouns in user card. (#5442, #5583) - Major: Release plugins alpha. (#5288) - Major: Improve high-DPI support on Windows. (#4868, #5391) -- Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746) +- Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746, #5643) - Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530) - Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625) - Minor: Moved tab visibility control to a submenu, without any toggle actions. (#5530) diff --git a/src/widgets/OverlayWindow.cpp b/src/widgets/OverlayWindow.cpp index f81ebf0ca..94811fa87 100644 --- a/src/widgets/OverlayWindow.cpp +++ b/src/widgets/OverlayWindow.cpp @@ -85,7 +85,8 @@ namespace chatterino { using namespace std::chrono_literals; -OverlayWindow::OverlayWindow(IndirectChannel channel) +OverlayWindow::OverlayWindow(IndirectChannel channel, + const QList &filterIDs) : QWidget(nullptr, Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint) #ifdef Q_OS_WIN @@ -116,6 +117,7 @@ OverlayWindow::OverlayWindow(IndirectChannel channel) }); this->channelView_.installEventFilter(this); + this->channelView_.setFilters(filterIDs); this->channelView_.setChannel(this->channel_.get()); this->channelView_.setIsOverlay(true); // use overlay colors this->channelView_.setAttribute(Qt::WA_TranslucentBackground); diff --git a/src/widgets/OverlayWindow.hpp b/src/widgets/OverlayWindow.hpp index 322dae468..f1c61362f 100644 --- a/src/widgets/OverlayWindow.hpp +++ b/src/widgets/OverlayWindow.hpp @@ -21,7 +21,7 @@ class OverlayWindow : public QWidget { Q_OBJECT public: - OverlayWindow(IndirectChannel channel); + OverlayWindow(IndirectChannel channel, const QList &filterIDs); ~OverlayWindow() override; OverlayWindow(const OverlayWindow &) = delete; OverlayWindow(OverlayWindow &&) = delete; diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 2356653d6..195e71c6b 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -1155,7 +1155,8 @@ void Split::showOverlayWindow() { if (!this->overlayWindow_) { - this->overlayWindow_ = new OverlayWindow(this->getIndirectChannel()); + this->overlayWindow_ = + new OverlayWindow(this->getIndirectChannel(), this->getFilters()); } this->overlayWindow_->show(); } From 2d818a7657f29475d3940b15a2eb393f20eb7e60 Mon Sep 17 00:00:00 2001 From: maliByatzes <130395400+maliByatzes@users.noreply.github.com> Date: Sat, 12 Oct 2024 12:35:39 +0200 Subject: [PATCH 05/40] Remember Popped-up Chat Size (#5635) --- CHANGELOG.md | 1 + src/common/ChatterinoSetting.hpp | 2 ++ src/singletons/Settings.hpp | 4 +++ src/util/RapidJsonSerializeQSize.hpp | 52 ++++++++++++++++++++++++++++ src/widgets/Window.cpp | 16 +++++++-- 5 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 src/util/RapidJsonSerializeQSize.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 82f4a511c..0a8b5eaa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - Minor: Links can now have prefixes and suffixes such as parentheses. (#5486, #5515) - Minor: Added support for scrolling in splits with touchscreen panning gestures. (#5524) - Minor: Removed experimental IRC support. (#5547) +- Minor: Remember last popup size for next popup. (#5635) - Minor: Moderators can now see which mods start and cancel raids. (#5563) - Minor: The emote popup now reloads when Twitch emotes are reloaded. (#5580) - Minor: Added `--login ` CLI argument to specify which account to start logged in as. (#5626) diff --git a/src/common/ChatterinoSetting.hpp b/src/common/ChatterinoSetting.hpp index 6dab1a0c6..35d538b13 100644 --- a/src/common/ChatterinoSetting.hpp +++ b/src/common/ChatterinoSetting.hpp @@ -3,6 +3,7 @@ #include "util/QMagicEnum.hpp" #include +#include #include namespace chatterino { @@ -55,6 +56,7 @@ using DoubleSetting = ChatterinoSetting; using IntSetting = ChatterinoSetting; using StringSetting = ChatterinoSetting; using QStringSetting = ChatterinoSetting; +using QSizeSetting = ChatterinoSetting; template class EnumSetting diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 3566815e8..94366dabe 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -212,6 +212,10 @@ public: BoolSetting useCustomFfzVipBadges = { "/appearance/badges/useCustomFfzVipBadges", true}; BoolSetting showBadgesSevenTV = {"/appearance/badges/seventv", true}; + QSizeSetting lastPopupSize = { + "/appearance/lastPopup/size", + {300, 500}, + }; /// Behaviour BoolSetting allowDuplicateMessages = {"/behaviour/allowDuplicateMessages", diff --git a/src/util/RapidJsonSerializeQSize.hpp b/src/util/RapidJsonSerializeQSize.hpp new file mode 100644 index 000000000..c5cff3778 --- /dev/null +++ b/src/util/RapidJsonSerializeQSize.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "util/RapidjsonHelpers.hpp" + +#include +#include + +namespace pajlada { + +template <> +struct Serialize { + static rapidjson::Value get(const QSize &value, + rapidjson::Document::AllocatorType &a) + { + rapidjson::Value ret(rapidjson::kObjectType); + + chatterino::rj::set(ret, "width", value.width(), a); + chatterino::rj::set(ret, "height", value.height(), a); + + return ret; + } +}; + +template <> +struct Deserialize { + static QSize get(const rapidjson::Value &value, bool *error = nullptr) + { + if (!value.IsObject()) + { + PAJLADA_REPORT_ERROR(error); + return {}; + } + + int width{}; + int height{}; + + if (!chatterino::rj::getSafe(value, "width", width)) + { + PAJLADA_REPORT_ERROR(error); + return {}; + } + if (!chatterino::rj::getSafe(value, "height", height)) + { + PAJLADA_REPORT_ERROR(error); + return {}; + } + + return {width, height}; + } +}; + +} // namespace pajlada diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index c095c9c28..4dcb14503 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -18,6 +18,7 @@ #include "singletons/Updates.hpp" #include "singletons/WindowManager.hpp" #include "util/InitUpdateButton.hpp" +#include "util/RapidJsonSerializeQSize.hpp" #include "widgets/AccountSwitchPopup.hpp" #include "widgets/dialogs/SettingsDialog.hpp" #include "widgets/dialogs/switcher/QuickSwitcherPopup.hpp" @@ -75,7 +76,13 @@ Window::Window(WindowType type, QWidget *parent) } else { - this->resize(int(300 * this->scale()), int(500 * this->scale())); + auto lastPopup = getSettings()->lastPopupSize.getValue(); + if (lastPopup.isEmpty()) + { + // The size in the setting was invalid, use the default value + lastPopup = getSettings()->lastPopupSize.getDefaultValue(); + } + this->resize(lastPopup.width(), lastPopup.height()); } this->signalHolder_.managedConnect(getApp()->getHotkeys()->onItemsUpdated, @@ -142,7 +149,12 @@ void Window::closeEvent(QCloseEvent *) getApp()->getWindows()->save(); getApp()->getWindows()->closeAll(); } - + else + { + QRect rect = this->getBounds(); + QSize newSize(rect.width(), rect.height()); + getSettings()->lastPopupSize.setValue(newSize); + } // Ensure selectedWindow_ is never an invalid pointer. // WindowManager will return the main window if no window is pointed to by // `selectedWindow_`. From 64864a0901a3773a8b04c83d14808748ade5c114 Mon Sep 17 00:00:00 2001 From: maliByatzes Date: Sat, 12 Oct 2024 19:42:24 +0200 Subject: [PATCH 06/40] Add name to contributors list (#5644) --- resources/avatars/maliByatzes.png | Bin 0 -> 17482 bytes resources/contributors.txt | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 resources/avatars/maliByatzes.png diff --git a/resources/avatars/maliByatzes.png b/resources/avatars/maliByatzes.png new file mode 100644 index 0000000000000000000000000000000000000000..693d84e05ef596f99fb8539c4d1bc81383bb3943 GIT binary patch literal 17482 zcmV)JK)b(*P)E=oio!r|`b=9OS&}%8^>u~5RVs_D!a<xIuN3 zujcvX%KoXZ{i18D%i!%2Kw#V8jF#l4L#RHQ#uH@8!!XOzB1=cfRCQ06weWQxgmLVh zo(_;x7)Gp=>=D&m5kZ!4*9-JUW?YShy|g3=!%h+qAx+ zeP@EAx77rBd3|5Auxf3@5VMq}JrTz+t(#!KGPT3eU^1CZr&CjKUUGVRIvR~4wKR#| zW{u+&DY5iEwB5)t4A5to~u5Vsxf8lgJ zs9jRE58gf#bY|UaCKa^{!$2(%OK(mUtgHzkc(|l`F^t56=sOi2ugE-$0f~PQQb%_RK%K zitN0=ePQ{_MQa@tLGI5Ve;WnhFNmSq&>>5o7g;{T(1dCwQ1H{!6HL&~&d!fN{`lqMOL}(q(D@=fw^B@UESEYy0Njr9kv1wu3ove zvb@&q_ezVQ?-t1NiT5r{KWypGs!q8T+a$O`wzeKU`r+WHSj2F=#=}2NdF&>`%OLUE^~< zsP@F06M%D5byt>W6Z5ELXiv1G78{Gfnpj-OpGKq8hYufq`|Y=1eDX2ILCpPZf}*i% ztX-=SS;QF08-)7ixka+Mm)qN_SxcSYHp69h$)sYBH=0YBrfb)(UESPVU%#@xzP@?= z2D^lJXtX+rVIrTMAXz_Y=j={m~{PfdL#|OKCq*9@}(x4%sC)(wsya+&pQ?A%oBb~~!YCvKi zmsdAaN)v$HQoZq~DD3DH{7{y-v$nQ&>)qR%*KXYY;Dc_ryS%d2>n-uYNmi84FTE5p zjxjYF1nXt`mB&^A=v4=nSH4{W2)wBTd1^7xN>-fvT;sK5Nd2X{NY?yZ~ezJK@b)hnBPE?Z2jSJbpORQKLM1rR_kUOP0CWlK-E zT3vPib_pQ#%?OnHz?O-Hz~G?xKm73U$&)7-{lQ?sx?>#HmU>*gxX@-XNjL*TTzc^ylY=A!AWxp{cLnjeK42FcRrg{9DHA9YU@XMEn<5C!=~qY+`} zWjjX^YqYhs!=hvEpFDZQ9TtcSLF3sL1dFRHJv=S?iws!(m_2|w3OvjS7v~p(@D!ry zz&Ik#jEArZ;%2q8i+Dk?^BSQ?w#qB3nXS&zq5sr77=%JsJ#-c%SKb zI3A9kJ$}_+xMRsaYBj0{H8*guK zfAPf^_wRrG`0->-nnxJyM!=^nuuGmLfBMfQG$8`@`1#^}TvCs}A?Q6}JBZt9Ok;lU!WGK;)K z_ar$GD9R?r9WG!+?51B4*zk6*J(D zuoLr;dh1*>hFM$e%18hbtcbwWwVu)wc+W6__`T(2(j2^f`tVBo7n+%jJN(E+C7<$2zb8x?z`RHT`Rm0Ut&_x z_g0gzv&nAgb^Ao+f)%s+T!bQ_UY_7mkt}W*QzSZG2&(Kr|Fa*LSAem&L^hTFAx?cmD40e%tN# ztBS%L0x4dxFz^~pP?;G<9%=8B$B)1J!?zl1N>sVjZ#UJ}i`#8TAl~hCv8|%|BwU{O zTAZRJ5QREh0bj)IWL3JK>s?>Xf93E4d6_4nk*5o)(Kvsq;odIE`_*gLS5_KT;LEx<{=D+p>oh^Cj70@F z%MGpH+J1%rus{87qti!a3Ia^bzQAD}alQC}R;$Y;D3BtEG(oo)DBQ!fk;GwJ@p1aY z3VT&DY(BVLe6_w7m52o74$B`RQm^UHnhAoQHIhj-E?-y!Yju+;#HryVUm(*BAqx}+ znb@`>CjSb9@g!j%cu#}vo#z-O;s_9tJTokaXgHNv_|2Li3k-@1oXXSL{?7K{-ac7r z(TUNcc1N@x+uCWjy1)%B3H)QJKN7mcNt#GrFK{G_D;cQjBTiI&axRgj006$$8YTOZ zt)M!s?B`-|?7IhG+qv?YD&XR7s zeSCZj6gD0WHLxM%hg#xMm0ep~0eB$gXZudmN#GF?p`23uNM+Lqh6(&&Rw0vkv<82c zRq-+pF*7duUT%WYm}poT_@$0Zol@LQs};0GW=NW^3Pj4g2d!2KR;vXHl)oY-rNlG< zOS?Xib`EYVLB5X~?y|=;yS&2%Jc(!UbEmUQRXT+>yAF>2( zO46xD&S1=$ENCdJ7UaR46y=;s5V(rDMe(H=JWqhPAT-;rn3xx+GxpS=OH^(wBMI=; zsj`HWSu}mb84xw{wmc#mPB*O}+A407dJA)jpD4VBz@<^5o`d)egidSygsheb>u!AY zEf8I<`z?b}gQcXZ#fU1ARd_U#49iI0!c6@>fu;xYVZPLgBQ3^*4oart2P|mXkiwZ5A`x(5 zp}o9?=UZ_t@?+U}M5}Sk>ThgZVN2<^Equq<^TIbK0A(12)s~m*=SvUdc1$8B=}e}9 zjcu2d)F=~@S|JdbAZ}p_fUj8=UUX(_E!rlQUfGN4XH(TtjTHqTk}V&%ECnrBzuG#+ z9nuPouY?rOq}T|kCT0WylB1js(S$+@FGK>NRd*~k(4D0ILkibyP(L8N;UhAy)9qfn zcKyow20@&pa{|4D>KNoZ6|Y#3xl9vOC-bmq(Z@OspNq;^NxW021T4VB z&NW~y;<1ucigeSNqoP`IvqB~UgC&~8Ym$`b;Dwav34&IZSFc|u1zQtSmPt%ZKDGUyKL-JHjK}*(~SRp`QHbF};nW~sBcx0IPVXTE&XmZMk z61s@lgRsu)EJRx3I(*#@K?Q`FfEAldjUZZ-DKvH@G=UW)AnkUp8&TqSc_dJ}tf}C95y(}qzrMPG6>5aBaWI5c2MU{=AtJxA9pF-8u4<%Cz-q#TCssfd zpOqKJWs8(>SShbAY!pNpI*PYwN;;Q{Wo3gP5Afos3jVm(RI0iFO%f$pElj+z$-obc z@M2-2zKd6Ja083kgt^u2AySG$rTG1l+)yRio)I8}Sn1aI?a(74f1$BmjAh>SPxzkzbs<KZi$oG!BJL7`ixB)!O!8(~9G z4XjYR4T}Q%bMwY6?2l9t($ZC1U0RLMYZkyPw7O_e(IbOU3j$b6KgCh;wS2Nf-PsdD z?a6G6xgJf&sBfdyPDo&>!sfo@0da3S6J#e6BqHi9^{56WTBagN!#W|aB%6Wl%hh}1 z!k-qhusoPfX3E3GfA|w6!KLjiZx_v0E-9H2|J7ZD@?}$5RQ{{QQ6a^UvG5$=HNHNo z9W}B>OH4`1XQX+V7W)z_6;1D8G7}Tm?iO*YxpHOo`mM0n$0tk+wNzYCHsZC77Rx~O zBYI;dNO|6wh6Hnr*XcEug3YehRuX-l%Px)u5N#@JGd7(HY}JZJ!OGzGGJ;eQ07e$E zO`&UzXQ!j-$?1^Fu40`cORFvbF|{Gvst)oGr2ywf?;g0lDid zrT+5m+qXBb-}FNV_yli@04(mV*Tqcx9BqcZF`dkgjt`N6@->?n0jbFV5-D$)pil}} zQG?6(L*KCInUpjGs$tl&;b?;5A0H16k53K{j*m_TIGFxQpZY$rmTn`W#<^)I+8*nc4C z(Il&Jc;6-g)cEzKB#2c~sidHd;@iff$#8UPSx=)4M%+_p5DOV3jx=71v@60VV2mXA z!2#lV(M?=sf!P^PXVXb0%#vx!Z*MR#rPlr#pZ&19w#?@a+wDe**K8~L5INK{5QaL2 zjsOZ=4>6^-1EhpWvXvIIBu^qw5O8@470njj3?%pJ=Jj{qd9Sy$TvTMNMFRMn6~F=- zVWFNjc7=*USdlkK&9eGIx7S};S*02b4p+>1eHHv5=`X1do1+`IMvxjC&1o1=Rj~JS$C)1nX?ayeP9jCAxT$0P>)rN+kzaOUxu$8&@{& zefaASfBCERjcX`GAR^4qtN$Br(6D6ExdCJ2qJTjD66$GBmV}5%fPk(`ZLH0Y^tzCg zO($F@3XkH2(!Qc46ciP8ZRV|Z50}_ZvqopyY@asU)2O6M7zQPhEbD^&H~#y|eU zf4zD0b_@>GY)xi)NNGqoin>3niH{2?-n0N_GwZrmBv~O6Lu)_$4e^h$$d6gK;-Ysk*toVOix2jm7~DB@*H zH4p%q06hoxr*$8h!9&9(Jw@Ymxe z=s#5TiHJ;udg0O)Z%hETF51T{Nids~Rlo#MAVbliM1YL$&ERPU@Suq$+TBj1n6L!h zS?XX;?UKx;N*O75Dj=IwGJOS~4Qt{-q-$t?qZ1nSN;{CkW)xe*DY#Kl_Qn)7_ElO{ z%oHXqdF>7k5sa1Y7jP^syztH&S2mYcR+{Y|F+r}rSG#Qpt!jN_Maj+j8U5OqCIACu z0wIeVo|P-0>o;4SYu9hQ|H0jZ!$X`cU$oVgyX*3&M(k=qsY$ma^w!nnmeD*=TSG9PPCdjTW)20cj%m}AM zE}r{zCBpEg{RmG$(U&-dp2bi{30V6Ip(KdA>0qnTCO%T!MwzlwkC)X!vo)LWl?d%q z%Ad4a*zwVHB&-&_Sas;?8Db7>%j`5vsNwo+dnk}MF?FMMG zK9t|3W_El$8BZ;BSm`gZik)6NwFVf8w%HzFN0|~43u~W)u7Z%`StvA=IgA=Xaxtn1 zcF+kx?#~5aHbeUWWCu!7&gd`-WCgaBO-Pbkw7OB;GKPwB_atO3%R3irf;XCo`O2$t z6dQ{~QEjsc=n%FVQV_@wmQ=el#w`8O!2$Q_nBdAWBf_>0eYyi0?FO+$$LmPe1sw`* zmiW!AQ1&(HQ8SB$0Oq$`nZ82(wEML-=Af*}Sc|>#@BF|o+XN*RNNr#WXp=dH#KM3e zfG&(5lfoyuUxupIlBW`o|NKR(f5}jUl{{E#)J~`$!gM3DVMQcVXK}lrS<8Ywupr+* zc)UswY1&%W2Q21i*?XmoAgxNKS%fbt$_(1I=}Iz7c!-!Ld1 z32Qm|yegn21YK@hOpSOH(!qC605^(q(`ZB_-`VzyX_a2r!w;ciJGk&DU@X={>X(=ivgL_CCbN{Tft?_{d{xe$RRXMv|J z?%Uk15C}y6lJ#;)wcHi*SAEkmzrpP)rk3AjG^|dGF`!R>uI$zH_M^@6vEovPcj?0u}YF`M2gZKNdT2<4Skv@;ma&fR?NzEEPzO}#P$M~22@j?OmlFOmjQw-rJ`(wL`w2n z<$S8nW?q#I7*$frGDT4Gl>Q?KV4*p({vgXrsKfPY(W=ejxj7J)TSYp7z*HDfNg-cH z(TIK*WmX|uL^8ZYn&=daE<^Yztr&9(2#R&~H8YY^NUuGS;4xj;fj zqz*)Rm4uia2NhW(}><%35(9(!zS;^{LurZ_EkLx=N+hKyZps z$Vg)K6=DcW;;jlRD_OTSPwv0XCa6BP!M-lK%<3Zp>ycymX3AqI2h3>6O!L;z3L`{* z&2~#CLjzZk36&de-Y{8efVSk%j1e99<={67`6Ht*#un#D zoiqJQYCbeciu@^ltTU>`yKEjsUk&+FM=ls{b}Ik)bIy$S*5LC}8|)VOjy4iJPh zKp6HC9-r#Xd;rxb%V~|U#$O3l^9t&4pHiGnvcy%)%IQG*9N!uxF=^~6et;C3I*x+9 zCEH5`+yL$58G-z?;AvAJ0`sBlFlBs)?~kSvd3hYOeJyJ?zf}_b-u^JuWKc17+6I|n ztI>S~K$vg#T|wz`o{dH?ja7Bq|^)`i^hVH6*irtnx3j10EN*nX61A_>Jmw6 z>8_M*6=D`tPe`RgA!|gf+&7$#Y?u5{@&#c?dvO1~3EiyP9*Gw~45RT_YyF`W-Ks81 z8))jkr2?qq?}m()QH>fCzBU426CeQV)l%>vwLw!np`j?LJ=tb0DTUOZ$Yu(aSE3uG z8r(`0q+^Jt7Fj7LF4gWZbL_ld!{S}-*rPJ0fR*ultr=5Y2O<=jdemymu!5F1I1R(G z@m<}tE&D{n+uu?FEEq?}RWbX+UXEnbH%tH|2W2vqmE{CCAy=9^rjzDKT|zUqv5KH@ zI^a`d!HT8p%k<(I;}okYh&owLYbo+tNxd6otwJ4YmIRqj8p^07zy$D{#xhAKP_fw_ z9YvakqhO|kQ<_d2Y~*ejc-4ryx6cIC8S2g%unIN^h}Gv}P*a%!*J>dHc7U)cWtP>% zVj>59%3`x26&Nf4WqRqel+UuZDKKXs&m?6$>0ozzmsAnoBdrYzk#FC8fHnT{I>tON@bOew zP^jJaG#~4rGo&u^jKvgHUr5K@ir@%Y!=}u0EQ#_SgS0^5Mx&?Cwhwmqo;>-9?tWf< zG8o#BYD-)!*3eOA;oIwk>-;!V9|XXpNCd!SJ_KO77#a$5gk=I6LNR69NY&(JG{vTr zl&#Vm+x1&YTeh8Qo+rSyuY?vg^lcq`z)S4D7_Km=dZ}6cNUfq4F8w@FG-x=Q9-a(N zhNH(%e?sMd{P8hkY4*1F_>we{@9nV@4!~$>GOl`_uEj|JA9k!R%`F)=|1Ry@f}hTj~Yocy%C^XR8%yZZ+Z9{v#m8ROL8(}-=vT8;&^ z+7@Wdt4IB zJx!)9ZO}#CVfdJV(iNI2b5!t3<_oN@tgkMwvF^q>lynNXQeXw@w?(Io9hsU<5CPF` z95um}*b-V4E8EI4bEHbm8-X&nr<&%E*fBxm* zXn+6ozyDM3pP=O?5@pFeMqy6-h14(_ZJYUv?0fAP_7i6CH4ESc6ePZZ7Gg_yM*z!I zlm(M)*(u))n}c|SpR%{dnnazDZ;g)g^5y;#V^vtkadHSUkP6P9H=~Ai*R%S(y+`}?8Iq719>qfQ`}ttN>N6yEfB%s6Me)frFk^h61^3qNZdr(J zi%&lJghyyf_}jnz+YjEqi%E1@z3mlC(~O+?*B3xuG22m%0z%aiY}%*6!etg8vbIULo;4?(YsykM$_jvFY?q(sf7L$Wyq|L?vM!)E1n$e@3fnf(_71(MmxSLRWB$DzMQ3?Q&5>$ zMw{8m5j_FpNu$*?zz*-cytd4yVOPkfMAGA7b+^%QMBfaRPFd(3nrM#jHz+>U9ol7W zc;(5&&L{A~at5W2B@QB&y9Cy;U9rt%zUy>K2@mqdbD6QcO_;rId`e4D$ zP{spD`BzvcGS&+70l>i8?d>1#A06ZVggO$(E9-0b?%i2mm)-*T(`a)oEsy;*|(W**Bn8#l|k+uJKF? z6wa<)+Y}B*+{6{zxPD{(xVFBwzPvOV1rwr}aUx1rPEV&&?~Uk5 zlBwIE{OMpcJRY2E?I780@*~mx!e>#w`~KmlpMSwPWJV(pf3Z?lHWQ$<5T=)yHC=i= zt-&h~+E5FlqpqYpS8?Lz0x$_x8$!tkPRO_kLl(jr!1#Vvlt zlbfo;D#jG;+_}bn`ji#Lb&=#{kTm`mDFI zA_$q6<#DUCT4PpTL9>4iCtS|rsE!QL0WVf}WQN@sA4sV4>8Bs#uZaAx&Qm+}i}6=^ zW(`^b?gSC%y4<__!M*oy)A)>LW;&(~m4|{jVw$s_kN`r-m^TIHvL7CO_wbKDeDm!e z*jv~)NxWl$JcrUdCTX8Jz+7gd=LQ{1f0x42U^E1U`|D1tq&> z=*zV$>)6oSx8EhNv7JQWP>UugZ5mNTMEje~;8*muM^)BsdrD}|;qBiKJaCgq2>#hP_&cZ{trX{1> z)yvGIylxZZS0i!3zgk9TgJR1;7~g;YJ*$t)HPs;aLon39^Mtk@NQN>3FPBQVU^>Fp zrT4khm54A+%5z`}ih^_s1kG-{+3(ZPH99%rJf$!1-$w?AgHvo#)23vM$C*%7a!WY0 zL{^mTE?NuIQJ{exgy0g(fu%BIx&==20Os_+l|ohwen~X znF+zgcr)pGYzWD}*A5nvU2&v( zl2ztt>D7Q7PseaYg+>g%msLBUKAEmMa!7|=o?q6Q80PoA8tJ+yXZ*eXJb-Z1LTQ`s zPO3><{LMGteEs#;oWEvJs6dZt**rbVCd}?9WPbPNjT_gl68K<GfwZqxOVpdJM|AK=b;GU}T{6zZ?73qY`wTI_$;SiMdG7&XHLKpaN+0neGR$5nmH zwNPNrIJ4*`n9uv~zyHf$+*?~>&{W7i=(U(UvXRfUl5XRceC1;4j5k_DcXoH#SWlil z1G-;bU7sn#)a(4KU{DC3CG%Up;ppK&i_(F4EE4RGN(@PSu5DzZAWcJ2eLXSi~_ zbI{GJ&ks}qSmnnZqV@A=ywQ0!mE9=Z8n(Rf)_6M520k9{#`k~xkrOe9IFBeP4W}}D z-)m^SOUA@eWTYJ>4aco07RDva$;i%e`X8{ z_X)z(<$lIdFv8tL0cGZ$9nCy7?s)0JTA3ZZNsNTPKY#LQl}_Gv^XirL$+Rb3rSwX+ zXJ$IvGNU*o&0$O7%$d;4w%9w0Wo8VvU2YGvHEP;krk zIXlaZ(A@Xu!$Iq-Uk~K#alN}a#lMhtc){@4ngH~?;#N6fsv1r8^CF_?CE=zkLwoZv zoRVbXcu{YKQ81P!?qVTq{*}dX95?oxHDvI?oT<7zHWkjn=ZX53_tf@bdBe? zOMNTYI6(U*D1@;qt8*3Sg1IySl*hPNbq3oCJh*?bd;jaNfnRp__f-ewLF_Z7LlUD1 zBn@+tmIuh_u`Zqt=ZnSG%Sp=Vsm?cHM(ycv1X>Se2w<7!d8jL`UVj^CVSAT@Yq3QV z+7nkwuybaE%j#Q~?mSvqT(yO@nA@myZiwn*9_xdt&+jA`q?!%D1yZCmQh~mY=lxvz z^q-D`EVRJe8(AET!Jr=DCFGgeA0=NF3a|@N0isQ2$tlf@UQ63Lvuaq3PB2dBgr4ov zA$fExm`|@u@txXPXKDHQGfGc%t#UCOK9k5uzh*1Yvyrr$B@b;bbY9YP=LeR#hWE3^ zeGy9W&h$+>Ih6w|-SILtA70u0^39EEMt!@bFM^~?m;Gp3z8wHf@4O_R3(kE(e8k<*Qn4b;9d|d93Z#QCbg(fao`SDT?qeN zQ^~@d>iUyy4w4-Esg!SfD>z}bA%&MDfH@INzm{gaTR-ZJiS$YZAdsBIGNzJXnXw8! zouJJ|n6{{@6Qtu$CHR&T7|?q7e|YEVL?Y2fUq;s25EE(sr=7GQPWowkx7~lb^<;bR z;1HSu+M@((cPlkUOQ61v%84k~n4+7M&>Z+>;Xm*bC{H-0=3z z8+Y!!&+1FRL@Jp6?$+Vn&MqfpQxHZH0CO;w$?~k-1=K+(v1XkL?RlTdwvmR8Kn|2; zs+Fb4Rs2)Mpf-y8oT*uG!Wa2ue}O;wq9D-eyOWBcJ$yGie#jcx4E3vJ7?*UAn|-h; z1?vA?Z&Ki9k|E;_XV6a4h7J~|0LZB@ckg`2;e@x}xqf}~Dq41Oe8_7N$<39kCpWI& zxUzC}b?N!jr(hK_M5xzYU*90nCefTsGd(MBqF5Yhoam8kjL;4ToC5>K3NvQ1PW}vi zn{aP+qFT7(SI0G+_XmX+oCT%N#6K6tAclI{OHetiKfU}GhAudK16(gH53qg*695hX z-^=pZa-?#swGg64r^8kwym$BR|NI~SZK>PB*uynB863&%Tl~aeLa+74a{tQ8Z+>(4 zj%^@M*`EZGL!s%W+xrI?`u&3=N|6wBho^fIM`bBr6PbdFA?V|K>LwNDC$9DbTZ{EvjIXdrQC86SB#K>8JZTKw>`kUU))hFcB^9k2 z1~Qk#&a_&XjIem#(wPv*&fYE-)W3c^x0A|$+Af3(!{N@HA5cw+3ccFg?4Qv&^D+}; zHqM$=>g6{Jen6KQy?yWAy${~IU8Ga0F>s~mxPbY?JO>URB9O;Maayv0=4jaI;P~M1 zSSBdaF3hvgxqv26sjz3l*Qc^<=Ux|c`V?*Tp=V?suf%+c%H{e>6!jKxn+vUZEl>8& zKl8#@RpOcdMMj9MuXD09s7XcG3cYd!MgH^fxadW!H zr>9_KE9=Yu@XP-MM}PNVkHKCtj9!km;IuF~$eNlhN3~5Q?xud2as)*?Xf1`wU^hRAXVr5pU@Hf;iIQoaxo9Ueo=9H*=4E3> zxsJ7Cuk=;vg7K+92z$IZ3qKHPHD%(Q!XK0de)FrpxwdhGVkh-%Xxf-mPpw@XwqiZs z?N!h7e3=O{pI$3cFhhZ=s>lVa z+VZe_0vkCM%9jcaBjLztq=~3Q4vb0x{80WKAB?vS54Vp_*l0m3l4CI&S$&NhZgaLU zTVKIf)3^nct$w@E9PzNw>r-*RckkZie0fv5>X@0obeho15;0Rfzymw~9ha+W66l0q z9oM|FEKf^y6hth?G0V}jax9)Kkxq8ZAqPs|3~^2_=Y{#w3MzCOhRk0}W+2o1oWsCg zF1b-bz3ZXAQjUr5a)Rp0^Mx?9{?`jDWiN_0y@eo<;R)M zUmZ4Zp89ol7?`j`gy<&67s<3$rZ{k_7{^{59-kf!Cr6xxF_{8H>u5HvL=QWeQxoz9 zB3?CxqnwIs{RfVXeC7L*5)ou$h%mNx}nK0Lb0!=0(S_<*U6$q*)Iml5D zmH00!47I4=z5zaftp}SEN-)9Wgn6}-F(+PtaYB`)iv@^c2qY1CEv-?>Soj2=I^{(6 znT(nzm@HW;{Gap-;1dL5_xR*!G&(#TN?}K-`%zWnt~*1oHhSAeQaYWj9UNaQ0KyAk zvb$3UJL}fpIpM`ZSWsAT5GAuwcg#K2-RB4;wh1z?2rt@-mK6uG zoFs9#eJrQA=oqnCI-X7YNi&W0g!N4L=P88Qh+a;i4zm7SH$A$oPB&_g{m~d1>>ccH z9}kAJbTXB}1EEfTbhc%|u-FJslZwf}n&RM`znn^ZUI29uybuMQe_RZ8M8i|gMclL)9=5I@X zUZ~`9O3RUhK-|$7HtlAi2eJBksWbYHgXId^{@I&D*3PHF&Xg^*yusGS@(AB6YJdp9 z1^VEFJ3U6^ciXgeSaXt`R;s7u7LEsSSy+%Grx1H#g`_6D^<0pN$pn~eXaC^iPd~@* zgPQFh9Zs2~%^^LjOZ`qekTLJ1U)j+K?Q!D)gmOL6aWu>Jk57&VBYqE+^dnVbBS^_1 zaN$B>*3k~!ovOhouXaN@BHVsE(70Z4Tv+S+KkGi=!lZ%npV_@(A#`Yn-|+SHh>51} z8CStp$?cm<+5vg)5a4pGo1MS-BaQ(~nTUf_xoJ?MidCGB43aHeba2day6~LMM;tRtHdFBk9cxkCmG2{C68}w^JQPo7m zK2Z+74PNmq!xsvmDDuh{7xgmPIGNF*#d7o2(CyvrZQ8QHH96uNlv9t^6e7=r9!H2k z5PqUg)au3UZq#feiNR#LwR^C$e|SjK=uCzSO9mP0K@B=^qo};D9l~UNMQ#Vw1JHUV zyb#X2Bg`D0>;fLg8<0EH1b7&)p++VdcYE^P9{%OVE72K z(T5Ko5Q4w+&TSdQIm=uxv2zKRmI-V5^h{uY0N#K9y@t+nl)7UaK7IV;_=Gtlqx~@4 z-rZr$4gnOiUdUN^`BHm%ZEbsH?J48G=ue@XaCmZz$7Y&PfyP5F$0CX?C*Z_>-*pQ!zcg#(et04eEIno!@)6}s(t?usepz` z5yA5gj>hnO8hS1WSHr{s%4tj^q!W)h8^YzU3L3TJ#O(~o$l>_)6UxpbX7ymHUP(P4 z`=4w3@|>A{5sSju?FRBVE-Z;BJt-f4_#qG(8JUI1_O)k8@t61jfBqw(NvKDAgxXsd z>BK1+q0dIgX7Je@(su2}jhnaM+dkO;X?NFawlYE^4iSsuSrn(VPQ(!mx)DGSbA2ep zL$voiDU|`~;MsI&b|B+_0vR74Dhm$M9JU8mmYy%k4?JIA=B#Q3(k8LmkM>wt4bZ-H zsD~$6-XMFIZ7PQi2!fl+P-8t%G4u_t%58pkVDr1Zir!!NXAhVrFw+@ZeqsH_(jow5 zN$9Z4D^OZ{>sQuV99zk`$Bkez9qGC1dF`jD?s$HAth!Dcs=o8jX;Tf$mU9_}WnO`8 zA-bTbp>@x?e8;#OHV@ZA$)G$7gmB)k0R}~N_T=1>ELe#J-7Gmd-Z_4QZYKGxBAQE{g?df6pG5ih#24aL{%iFqYO zMe}5vOQuJFh2{)x9&X^3CdF=ID>J_uL>rpJR7>p{)~I8i!-YGAlG;TQ*pa%c>Pt}wT!nKR}}I`pvcVHu*t)uaFgIm&>+C{@)-UZw9Xj|unv zvxVHn;Ac*U%8R;G>ORc($_^w6U@8G3bv+rOx6^U4CSA@g<&sf|kk1_b&0gK%hO$ASch2UuWG&V#Y}VixWA zVO^cQ@(?(`l8|4|Kh85`>$b>vITt?DS|sN$398(<2%G?1is1Qk=e!_*dT>Ans-2zXM)ywGN?yR0W&SGO?Ekh2l(|t{K_G)rX{Bpm(fpQsd zU9T^U)-U9D+DQ(ToQisTuYIw&f(nA7HebLmT)N}uXIR!s2EKqYEnQQynYFVa1HL?H zaY-M3tvkSWpAc_@$oS8Y<}CAcX{yVE&Nsz5)8W!l8&Vm^ky}%@0~V?R-bDj3>U+g? zJubYY-pfaxSN^r`V}iQRFX~$@caiH^8JOz8SEE@G(l~xtCzS^AD`G*fT*#+2&A;xCyRTi^wF;Z>7@M2Y9q1SG~)VP-{Znbl_ zI*hUK4(?A2zw3kyH%Z-ruQs}daERX}^Ys!7G8bE6WvipJGX6c!`D`|U1pw~2s(Np9 zy`)v50!8ZTSvKU^mD-j6QX?W(jXL%B`ICK+-r279l2}-6r_8m?*FX*_D%!;R$_LcN zQ}+QCGEr7T$ws*ZTGfe#V9Y_-vZ>6ak&u>g&+c5B4{+#;r*pUEOlAdP&NI>KrNsLE zA}<_~&$a*6(T*39+FnxddN~8dIfi=1t6h|pw2Jwl_wwYKIhE0U))@n6nn7vACX?WV z2}XrJ(rupQRs4z0nO8PJLM;(uuG~4;+dnxv9_s+}GPTn`2?t#7SQ6kC*Y4-lDYKRP zjohOAnFpu6oYDW1PK||d_C*G&j<%i$>Fj3Fpmnd{1#tsR%dis>7MQ6oSCm&4K$XNw z$0UI;pKWK2oM%-!8|v(UGfi$Jxs6+>KjFgFzpz^Js$9$$Hm<%P$~q6h)`eFN<)p>4 zBL{fXu#O?3i^2|zaz>&~(YPD|*n;vg*dGKS2cc>cS)Gbp7r@!#Nnw>Ka{%RGJz==e zdc=-a*LSSM? zLfW8+3sd&GQr8DE8%*R@zaGQ9$lRYF2zFr$r=fcSie{%JIh6#F+ zM%V5B!rpz64=4c-C2$xzKt=|4$gG0fVGF+G5;9?z=D1eSi-NYr`;IUoldSXt>Y&>K z{P~hV=uhhYi%8Muc19hAui5v!sE1F6xL6cazgfo$@Rcc+ozn$EhcmElA{*v+A*ffs Z{eKR<$k6>TuI2y$002ovPDHLkV1f&3|Lgz& literal 0 HcmV?d00001 diff --git a/resources/contributors.txt b/resources/contributors.txt index 6267887e3..d6eaa1a7d 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -35,7 +35,7 @@ Confuseh | https://github.com/Confuseh | ch-ems | https://github.com/ch-ems | Bur0k | https://github.com/Bur0k | nuuls | https://github.com/nuuls | -Chronophylos | https://github.com/Chronophylos | +Chronophylos | https://github.com/Chronophylos | Ckath | https://github.com/Ckath | matijakevic | https://github.com/matijakevic | nforro | https://github.com/nforro | @@ -79,6 +79,7 @@ KleberPF | https://github.com/KleberPF | nealxm | https://github.com/nealxm | :/avatars/nealxm.png Niller2005 | https://github.com/Niller2005 | :/avatars/niller2005.png JakeRYW | https://github.com/JakeRYW | :/avatars/jakeryw.png +maliByatzes | https://github.com/maliByatzes | :/avatars/maliByatzes.png # If you are a contributor add yourself above this line From 3e2116629a53322451ca02627e8f67e7ba64b147 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 13 Oct 2024 12:08:32 +0200 Subject: [PATCH 07/40] chore: improve appearance of Twitch emotes in popup (#5632) --- CHANGELOG.md | 2 +- src/providers/twitch/TwitchEmotes.cpp | 15 ++++++--------- src/widgets/dialogs/EmotePopup.cpp | 25 ++++++++++++++++--------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a8b5eaa4..2c71db4ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,7 @@ - Bugfix: Fixed splits staying paused after unfocusing Chatterino in certain configurations. (#5504, #5637) - Bugfix: Links with invalid characters in the domain are no longer detected. (#5509) - Bugfix: Fixed janky selection for messages with RTL segments (selection is still wrong, but consistently wrong). (#5525) -- Bugfix: Fixed event emotes not showing up in autocomplete and popups. (#5239, #5580, #5582) +- Bugfix: Fixed event emotes not showing up in autocomplete and popups. (#5239, #5580, #5582, #5632) - Bugfix: Fixed tab visibility being controllable in the emote popup. (#5530) - Bugfix: Fixed account switch not being saved if no other settings were changed. (#5558) - Bugfix: Fixed some tooltips not being readable. (#5578) diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index 3389a43ed..005aff341 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -494,17 +494,14 @@ TwitchEmoteSetMeta getTwitchEmoteSetMeta(const HelixChannelEmote &emote) return u"x-c2-globals"_s; } - if (!emote.setID.isEmpty()) + // some bit emote-sets have an id, but we want to combine them into a + // single set + if (isBits) { - return emote.setID; + return TWITCH_BIT_EMOTE_SET_PREFIX % emote.ownerID; } - - if (isSub) - { - return TWITCH_SUB_EMOTE_SET_PREFIX % emote.ownerID; - } - // isBits - return TWITCH_BIT_EMOTE_SET_PREFIX % emote.ownerID; + // isSub + return TWITCH_SUB_EMOTE_SET_PREFIX % emote.ownerID; }(); return { diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index 8b62b0e02..02a98bde2 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -136,25 +136,32 @@ void addTwitchEmoteSets(const std::shared_ptr &local, MessageElementFlag::TwitchEmote); } - // Put current channel emotes at the top + std::vector< + std::pair>> + sortedSets; + sortedSets.reserve(sets->size()); for (const auto &[_id, set] : *sets) { if (set.owner->id == currentChannelID) { + // Put current channel emotes at the top addEmotes(subChannel, set.emotes, set.title(), MessageElementFlag::TwitchEmote); } + else + { + sortedSets.emplace_back(set.title(), std::cref(set)); + } } - for (const auto &[id, set] : *sets) - { - if (set.owner->id == currentChannelID) - { - continue; - } + std::ranges::sort(sortedSets, [](const auto &a, const auto &b) { + return a.first.compare(b.first, Qt::CaseInsensitive) < 0; + }); - addEmotes(set.isSubLike ? subChannel : globalChannel, set.emotes, - set.title(), MessageElementFlag::TwitchEmote); + for (const auto &[title, set] : sortedSets) + { + addEmotes(set.get().isSubLike ? subChannel : globalChannel, + set.get().emotes, title, MessageElementFlag::TwitchEmote); } } From d9453313b365abd77a374a76b9a979d99870cac9 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 13 Oct 2024 12:38:10 +0200 Subject: [PATCH 08/40] test: add snapshot tests for MessageBuilder (#5598) --- .prettierignore | 1 + CHANGELOG.md | 1 + mocks/include/mocks/ChatterinoBadges.hpp | 16 +- mocks/include/mocks/TwitchIrcServer.hpp | 30 +- mocks/include/mocks/UserData.hpp | 22 +- src/CMakeLists.txt | 3 + src/common/ChatterinoSetting.hpp | 1 - src/controllers/ignores/IgnorePhrase.cpp | 8 +- src/messages/MessageBuilder.cpp | 9 +- src/providers/ffz/FfzBadges.cpp | 27 + src/providers/ffz/FfzBadges.hpp | 3 + src/providers/twitch/IrcMessageHandler.cpp | 23 +- src/providers/twitch/TwitchAccount.cpp | 17 + src/providers/twitch/TwitchAccount.hpp | 11 +- src/providers/twitch/TwitchBadges.hpp | 2 +- src/providers/twitch/TwitchChannel.cpp | 204 +++--- src/providers/twitch/TwitchChannel.hpp | 16 + src/util/Helpers.cpp | 13 + src/util/Helpers.hpp | 2 + src/util/IrcHelpers.cpp | 72 +++ src/util/IrcHelpers.hpp | 38 +- tests/CMakeLists.txt | 4 + .../snapshots/MessageBuilder/IRC/action.json | 191 ++++++ .../MessageBuilder/IRC/all-usernames.json | 315 +++++++++ .../MessageBuilder/IRC/bad-emotes.json | 159 +++++ .../MessageBuilder/IRC/bad-emotes2.json | 159 +++++ .../MessageBuilder/IRC/bad-emotes3.json | 135 ++++ .../MessageBuilder/IRC/bad-emotes4.json | 135 ++++ .../MessageBuilder/IRC/badges-invalid.json | 228 +++++++ .../snapshots/MessageBuilder/IRC/badges.json | 229 +++++++ .../MessageBuilder/IRC/blocked-user.json | 5 + .../snapshots/MessageBuilder/IRC/cheer1.json | 596 ++++++++++++++++++ .../snapshots/MessageBuilder/IRC/cheer2.json | 261 ++++++++ .../snapshots/MessageBuilder/IRC/cheer3.json | 211 +++++++ .../snapshots/MessageBuilder/IRC/cheer4.json | 141 +++++ .../MessageBuilder/IRC/custom-mod.json | 133 ++++ .../MessageBuilder/IRC/custom-vip.json | 143 +++++ .../MessageBuilder/IRC/emote-emoji.json | 257 ++++++++ tests/snapshots/MessageBuilder/IRC/emote.json | 159 +++++ .../snapshots/MessageBuilder/IRC/emotes.json | 227 +++++++ .../snapshots/MessageBuilder/IRC/emotes2.json | 485 ++++++++++++++ .../snapshots/MessageBuilder/IRC/emotes3.json | 284 +++++++++ .../snapshots/MessageBuilder/IRC/emotes4.json | 169 +++++ .../snapshots/MessageBuilder/IRC/emotes5.json | 169 +++++ .../MessageBuilder/IRC/first-msg.json | 161 +++++ .../MessageBuilder/IRC/highlight1.json | 152 +++++ .../MessageBuilder/IRC/highlight2.json | 179 ++++++ .../MessageBuilder/IRC/highlight3.json | 164 +++++ .../MessageBuilder/IRC/hype-chat-invalid.json | 162 +++++ .../MessageBuilder/IRC/hype-chat0.json | 254 ++++++++ .../MessageBuilder/IRC/hype-chat1.json | 406 ++++++++++++ .../MessageBuilder/IRC/hype-chat2.json | 404 ++++++++++++ .../MessageBuilder/IRC/ignore-block1.json | 163 +++++ .../MessageBuilder/IRC/ignore-block2.json | 32 + .../MessageBuilder/IRC/ignore-infinite.json | 225 +++++++ .../MessageBuilder/IRC/ignore-replace.json | 540 ++++++++++++++++ .../MessageBuilder/IRC/justinfan.json | 122 ++++ tests/snapshots/MessageBuilder/IRC/links.json | 239 +++++++ .../MessageBuilder/IRC/mentions.json | 173 +++++ tests/snapshots/MessageBuilder/IRC/mod.json | 215 +++++++ .../MessageBuilder/IRC/nickname.json | 132 ++++ .../snapshots/MessageBuilder/IRC/no-nick.json | 122 ++++ .../snapshots/MessageBuilder/IRC/no-tags.json | 139 ++++ .../IRC/redeemed-highlight.json | 190 ++++++ .../MessageBuilder/IRC/reply-action.json | 191 ++++++ .../MessageBuilder/IRC/reply-block.json | 217 +++++++ .../IRC/reply-blocked-user.json | 187 ++++++ .../MessageBuilder/IRC/reply-child.json | 192 ++++++ .../MessageBuilder/IRC/reply-ignore.json | 225 +++++++ .../MessageBuilder/IRC/reply-no-prev.json | 178 ++++++ .../MessageBuilder/IRC/reply-root.json | 195 ++++++ .../MessageBuilder/IRC/reply-single.json | 310 +++++++++ .../MessageBuilder/IRC/reward-bits.json | 305 +++++++++ .../IRC/reward-blocked-user.json | 5 + .../MessageBuilder/IRC/reward-empty.json | 240 +++++++ .../MessageBuilder/IRC/reward-known.json | 251 ++++++++ .../MessageBuilder/IRC/reward-unknown.json | 137 ++++ .../MessageBuilder/IRC/rm-deleted.json | 273 ++++++++ .../IRC/shared-chat-emotes.json | 392 ++++++++++++ .../MessageBuilder/IRC/shared-chat-known.json | 161 +++++ .../IRC/shared-chat-same-channel.json | 161 +++++ .../IRC/shared-chat-unknown.json | 161 +++++ .../snapshots/MessageBuilder/IRC/simple.json | 161 +++++ .../IRC/username-localized.json | 119 ++++ .../IRC/username-localized2.json | 119 ++++ .../MessageBuilder/IRC/username.json | 119 ++++ tests/snapshots/MessageBuilder/IRC/vip.json | 143 +++++ tests/src/MessageBuilder.cpp | 576 +++++++++++++++-- tests/src/lib/Snapshot.cpp | 238 +++++++ tests/src/lib/Snapshot.hpp | 135 ++++ 90 files changed, 14661 insertions(+), 218 deletions(-) create mode 100644 src/util/IrcHelpers.cpp create mode 100644 tests/snapshots/MessageBuilder/IRC/action.json create mode 100644 tests/snapshots/MessageBuilder/IRC/all-usernames.json create mode 100644 tests/snapshots/MessageBuilder/IRC/bad-emotes.json create mode 100644 tests/snapshots/MessageBuilder/IRC/bad-emotes2.json create mode 100644 tests/snapshots/MessageBuilder/IRC/bad-emotes3.json create mode 100644 tests/snapshots/MessageBuilder/IRC/bad-emotes4.json create mode 100644 tests/snapshots/MessageBuilder/IRC/badges-invalid.json create mode 100644 tests/snapshots/MessageBuilder/IRC/badges.json create mode 100644 tests/snapshots/MessageBuilder/IRC/blocked-user.json create mode 100644 tests/snapshots/MessageBuilder/IRC/cheer1.json create mode 100644 tests/snapshots/MessageBuilder/IRC/cheer2.json create mode 100644 tests/snapshots/MessageBuilder/IRC/cheer3.json create mode 100644 tests/snapshots/MessageBuilder/IRC/cheer4.json create mode 100644 tests/snapshots/MessageBuilder/IRC/custom-mod.json create mode 100644 tests/snapshots/MessageBuilder/IRC/custom-vip.json create mode 100644 tests/snapshots/MessageBuilder/IRC/emote-emoji.json create mode 100644 tests/snapshots/MessageBuilder/IRC/emote.json create mode 100644 tests/snapshots/MessageBuilder/IRC/emotes.json create mode 100644 tests/snapshots/MessageBuilder/IRC/emotes2.json create mode 100644 tests/snapshots/MessageBuilder/IRC/emotes3.json create mode 100644 tests/snapshots/MessageBuilder/IRC/emotes4.json create mode 100644 tests/snapshots/MessageBuilder/IRC/emotes5.json create mode 100644 tests/snapshots/MessageBuilder/IRC/first-msg.json create mode 100644 tests/snapshots/MessageBuilder/IRC/highlight1.json create mode 100644 tests/snapshots/MessageBuilder/IRC/highlight2.json create mode 100644 tests/snapshots/MessageBuilder/IRC/highlight3.json create mode 100644 tests/snapshots/MessageBuilder/IRC/hype-chat-invalid.json create mode 100644 tests/snapshots/MessageBuilder/IRC/hype-chat0.json create mode 100644 tests/snapshots/MessageBuilder/IRC/hype-chat1.json create mode 100644 tests/snapshots/MessageBuilder/IRC/hype-chat2.json create mode 100644 tests/snapshots/MessageBuilder/IRC/ignore-block1.json create mode 100644 tests/snapshots/MessageBuilder/IRC/ignore-block2.json create mode 100644 tests/snapshots/MessageBuilder/IRC/ignore-infinite.json create mode 100644 tests/snapshots/MessageBuilder/IRC/ignore-replace.json create mode 100644 tests/snapshots/MessageBuilder/IRC/justinfan.json create mode 100644 tests/snapshots/MessageBuilder/IRC/links.json create mode 100644 tests/snapshots/MessageBuilder/IRC/mentions.json create mode 100644 tests/snapshots/MessageBuilder/IRC/mod.json create mode 100644 tests/snapshots/MessageBuilder/IRC/nickname.json create mode 100644 tests/snapshots/MessageBuilder/IRC/no-nick.json create mode 100644 tests/snapshots/MessageBuilder/IRC/no-tags.json create mode 100644 tests/snapshots/MessageBuilder/IRC/redeemed-highlight.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reply-action.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reply-block.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reply-blocked-user.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reply-child.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reply-ignore.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reply-no-prev.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reply-root.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reply-single.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reward-bits.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reward-blocked-user.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reward-empty.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reward-known.json create mode 100644 tests/snapshots/MessageBuilder/IRC/reward-unknown.json create mode 100644 tests/snapshots/MessageBuilder/IRC/rm-deleted.json create mode 100644 tests/snapshots/MessageBuilder/IRC/shared-chat-emotes.json create mode 100644 tests/snapshots/MessageBuilder/IRC/shared-chat-known.json create mode 100644 tests/snapshots/MessageBuilder/IRC/shared-chat-same-channel.json create mode 100644 tests/snapshots/MessageBuilder/IRC/shared-chat-unknown.json create mode 100644 tests/snapshots/MessageBuilder/IRC/simple.json create mode 100644 tests/snapshots/MessageBuilder/IRC/username-localized.json create mode 100644 tests/snapshots/MessageBuilder/IRC/username-localized2.json create mode 100644 tests/snapshots/MessageBuilder/IRC/username.json create mode 100644 tests/snapshots/MessageBuilder/IRC/vip.json create mode 100644 tests/src/lib/Snapshot.cpp create mode 100644 tests/src/lib/Snapshot.hpp diff --git a/.prettierignore b/.prettierignore index 89270b789..b763c2ddd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,7 @@ resources/*.json benchmarks/resources/*.json tests/resources/*.json +tests/snapshots/**/*.json # ...themes should be prettified for readability. !resources/themes/*.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c71db4ef..5cef1eba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,7 @@ - Dev: Added more tests for input completion. (#5604) - Dev: Refactored legacy Unicode zero-width-joiner replacement. (#5594) - Dev: The JSON output when copying a message (SHIFT + right-click) is now more extensive. (#5600) +- Dev: Added more tests for message building. (#5598) - Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607) - Dev: `GIFTimer` is no longer initialized in tests. (#5608) - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) diff --git a/mocks/include/mocks/ChatterinoBadges.hpp b/mocks/include/mocks/ChatterinoBadges.hpp index 9070a7d7e..2f8279bc5 100644 --- a/mocks/include/mocks/ChatterinoBadges.hpp +++ b/mocks/include/mocks/ChatterinoBadges.hpp @@ -2,6 +2,8 @@ #include "providers/chatterino/ChatterinoBadges.hpp" +#include + namespace chatterino::mock { class ChatterinoBadges : public IChatterinoBadges @@ -9,9 +11,21 @@ class ChatterinoBadges : public IChatterinoBadges public: std::optional getBadge(const UserId &id) override { - (void)id; + auto it = this->users.find(id); + if (it != this->users.end()) + { + return it->second; + } return std::nullopt; } + + void setBadge(UserId id, EmotePtr emote) + { + this->users.emplace(std::move(id), std::move(emote)); + } + +private: + std::unordered_map users; }; } // namespace chatterino::mock diff --git a/mocks/include/mocks/TwitchIrcServer.hpp b/mocks/include/mocks/TwitchIrcServer.hpp index bbeba8ca4..d218192b3 100644 --- a/mocks/include/mocks/TwitchIrcServer.hpp +++ b/mocks/include/mocks/TwitchIrcServer.hpp @@ -7,8 +7,11 @@ #include "providers/seventv/eventapi/Dispatch.hpp" #include "providers/seventv/eventapi/Message.hpp" #include "providers/seventv/SeventvEmotes.hpp" +#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" +#include + namespace chatterino::mock { class MockTwitchIrcServer : public ITwitchIrcServer @@ -67,7 +70,30 @@ public: std::shared_ptr getChannelOrEmptyByID( const QString &channelID) override { - return {}; + // XXX: this is the same as in TwitchIrcServer::getChannelOrEmptyByID + for (const auto &[name, weakChannel] : this->mockChannels) + { + auto channel = weakChannel.lock(); + if (!channel) + { + continue; + } + + auto twitchChannel = + std::dynamic_pointer_cast(channel); + if (!twitchChannel) + { + continue; + } + + if (twitchChannel->roomId() == channelID && + twitchChannel->getName().count(':') < 2) + { + return channel; + } + } + + return Channel::getEmpty(); } void dropSeventvChannel(const QString &userID, @@ -123,6 +149,8 @@ public: ChannelPtr liveChannel; ChannelPtr automodChannel; QString lastUserThatWhisperedMe{"forsen"}; + + std::unordered_map> mockChannels; }; } // namespace chatterino::mock diff --git a/mocks/include/mocks/UserData.hpp b/mocks/include/mocks/UserData.hpp index 62159a19f..bf53ea4ab 100644 --- a/mocks/include/mocks/UserData.hpp +++ b/mocks/include/mocks/UserData.hpp @@ -2,6 +2,8 @@ #include "controllers/userdata/UserDataController.hpp" +#include + namespace chatterino::mock { class UserDataController : public IUserDataController @@ -13,6 +15,11 @@ public: // If the user does not have any extra data, return none std::optional getUser(const QString &userID) const override { + auto it = this->userMap.find(userID); + if (it != this->userMap.end()) + { + return it->second; + } return std::nullopt; } @@ -20,8 +27,21 @@ public: void setUserColor(const QString &userID, const QString &colorString) override { - // do nothing + auto it = this->userMap.find(userID); + if (it != this->userMap.end()) + { + it->second.color = QColor(colorString); + } + else + { + this->userMap.emplace(userID, UserData{ + .color = QColor(colorString), + }); + } } + +private: + std::unordered_map userMap; }; } // namespace chatterino::mock diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d24a0155e..5c71aff95 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -497,6 +497,8 @@ set(SOURCE_FILES util/InitUpdateButton.hpp util/IpcQueue.cpp util/IpcQueue.hpp + util/IrcHelpers.cpp + util/IrcHelpers.hpp util/LayoutHelper.cpp util/LayoutHelper.hpp util/LoadPixmap.cpp @@ -969,6 +971,7 @@ target_compile_definitions(${LIBRARY_PROJECT} PUBLIC IRC_STATIC IRC_NAMESPACE=Communi $<$:_WIN32_WINNT=0x0A00> # Windows 10 + $<$:CHATTERINO_WITH_TESTS> ) if (USE_SYSTEM_QTKEYCHAIN) diff --git a/src/common/ChatterinoSetting.hpp b/src/common/ChatterinoSetting.hpp index 35d538b13..f2006e6d7 100644 --- a/src/common/ChatterinoSetting.hpp +++ b/src/common/ChatterinoSetting.hpp @@ -73,7 +73,6 @@ public: _registerSetting(this->getData()); } - template EnumSetting &operator=(Enum newValue) { this->setValue(Underlying(newValue)); diff --git a/src/controllers/ignores/IgnorePhrase.cpp b/src/controllers/ignores/IgnorePhrase.cpp index 9f81fbc6e..41a49ec58 100644 --- a/src/controllers/ignores/IgnorePhrase.cpp +++ b/src/controllers/ignores/IgnorePhrase.cpp @@ -95,11 +95,11 @@ bool IgnorePhrase::containsEmote() const { if (!this->emotesChecked_) { - const auto &accvec = getApp()->getAccounts()->twitch.accounts; - for (const auto &acc : accvec) + auto accemotes = + getApp()->getAccounts()->twitch.getCurrent()->accessEmotes(); + if (*accemotes) { - const auto &accemotes = *acc->accessEmotes(); - for (const auto &emote : *accemotes) + for (const auto &emote : **accemotes) { if (this->replace_.contains(emote.first.string, Qt::CaseSensitive)) diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 41048fb7e..3802497b0 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include @@ -1820,10 +1821,12 @@ MessagePtr MessageBuilder::buildHypeChatMessage( // actualAmount = amount * 10^(-exponent) double actualAmount = std::pow(10.0, double(-exponent)) * double(amount); - subtitle += QLocale::system().toCurrencyString(actualAmount, currency); - MessageBuilder builder(systemMessage, parseTagString(subtitle), - calculateMessageTime(message).time()); + auto locale = getSystemLocale(); + subtitle += locale.toCurrencyString(actualAmount, currency); + + auto dt = calculateMessageTime(message); + MessageBuilder builder(systemMessage, parseTagString(subtitle), dt.time()); builder->flags.set(MessageFlag::ElevatedMessage); return builder.release(); } diff --git a/src/providers/ffz/FfzBadges.cpp b/src/providers/ffz/FfzBadges.cpp index fb4358ab9..c300efa43 100644 --- a/src/providers/ffz/FfzBadges.cpp +++ b/src/providers/ffz/FfzBadges.cpp @@ -1,5 +1,6 @@ #include "providers/ffz/FfzBadges.hpp" +#include "Application.hpp" #include "common/network/NetworkRequest.hpp" #include "common/network/NetworkResult.hpp" #include "messages/Emote.hpp" @@ -109,4 +110,30 @@ void FfzBadges::load() .execute(); } +void FfzBadges::registerBadge(int badgeID, Badge badge) +{ + assert(getApp()->isTest()); + + std::unique_lock lock(this->mutex_); + + this->badges.emplace(badgeID, std::move(badge)); +} + +void FfzBadges::assignBadgeToUser(const UserId &userID, int badgeID) +{ + assert(getApp()->isTest()); + + std::unique_lock lock(this->mutex_); + + auto it = this->userBadges.find(userID.string); + if (it != this->userBadges.end()) + { + it->second.emplace(badgeID); + } + else + { + this->userBadges.emplace(userID.string, std::set{badgeID}); + } +} + } // namespace chatterino diff --git a/src/providers/ffz/FfzBadges.hpp b/src/providers/ffz/FfzBadges.hpp index 761111831..4219cd035 100644 --- a/src/providers/ffz/FfzBadges.hpp +++ b/src/providers/ffz/FfzBadges.hpp @@ -30,6 +30,9 @@ public: std::vector getUserBadges(const UserId &id); std::optional getBadge(int badgeID) const; + void registerBadge(int badgeID, Badge badge); + void assignBadgeToUser(const UserId &userID, int badgeID); + void load(); private: diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 7b093881f..8bac0d9fe 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -632,15 +632,6 @@ std::vector parsePrivMessage(Channel *channel, builder.triggerHighlights(); } - if (message->tags().contains(u"pinned-chat-paid-amount"_s)) - { - auto ptr = MessageBuilder::buildHypeChatMessage(message); - if (ptr) - { - builtMessages.emplace_back(std::move(ptr)); - } - } - return builtMessages; } @@ -676,6 +667,11 @@ std::vector IrcMessageHandler::parseMessageWithReply( QString content = privMsg->content(); int messageOffset = stripLeadingReplyMention(privMsg->tags(), content); MessageParseArgs args; + auto tags = privMsg->tags(); + if (const auto it = tags.find("custom-reward-id"); it != tags.end()) + { + args.channelPointRewardId = it.value().toString(); + } MessageBuilder builder(channel, message, args, content, privMsg->isAction()); builder.setMessageOffset(messageOffset); @@ -688,6 +684,15 @@ std::vector IrcMessageHandler::parseMessageWithReply( builder.triggerHighlights(); } + if (message->tags().contains(u"pinned-chat-paid-amount"_s)) + { + auto ptr = MessageBuilder::buildHypeChatMessage(privMsg); + if (ptr) + { + builtMessages.emplace_back(std::move(ptr)); + } + } + return builtMessages; } diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 5a66c7231..3dbbe7a10 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -174,6 +174,17 @@ void TwitchAccount::unblockUser(const QString &userId, const QObject *caller, std::move(onFailure)); } +void TwitchAccount::blockUserLocally(const QString &userID) +{ + assertInGuiThread(); + assert(getApp()->isTest()); + + TwitchUser blockedUser; + blockedUser.id = userID; + this->ignores_.insert(blockedUser); + this->ignoresUserIds_.insert(blockedUser.id); +} + const std::unordered_set &TwitchAccount::blocks() const { assertInGuiThread(); @@ -336,6 +347,12 @@ SharedAccessGuard> TwitchAccount::accessEmotes() return this->emotes_.accessConst(); } +void TwitchAccount::setEmotes(std::shared_ptr emotes) +{ + assert(getApp()->isTest()); + *this->emotes_.access() = std::move(emotes); +} + std::optional TwitchAccount::twitchEmote(const EmoteName &name) const { auto emotes = this->emotes_.accessConst(); diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index f77d60df1..fe191a850 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -71,6 +71,8 @@ public: std::function onSuccess, std::function onFailure); + void blockUserLocally(const QString &userID); + [[nodiscard]] const std::unordered_set &blocks() const; [[nodiscard]] const std::unordered_set &blockedUserIds() const; @@ -83,16 +85,21 @@ public: /// Returns true if the account has access to the given emote set bool hasEmoteSet(const EmoteSetId &id) const; - /// Return a map of emote sets the account has access to + /// Returns a map of emote sets the account has access to /// /// Key being the emote set ID, and contents being information about the emote set /// and the emotes contained in the emote set SharedAccessGuard> accessEmoteSets() const; - /// Return a map of emotes the account has access to + /// Returns a map of emotes the account has access to SharedAccessGuard> accessEmotes() const; + /// Sets the emotes this account has access to + /// + /// This should only be used in tests. + void setEmotes(std::shared_ptr emotes); + /// Return the emote by emote name if the account has access to the emote std::optional twitchEmote(const EmoteName &name) const; diff --git a/src/providers/twitch/TwitchBadges.hpp b/src/providers/twitch/TwitchBadges.hpp index b9519a906..287e8bcae 100644 --- a/src/providers/twitch/TwitchBadges.hpp +++ b/src/providers/twitch/TwitchBadges.hpp @@ -45,10 +45,10 @@ public: void loadTwitchBadges(); -private: /// Loads the badges shipped with Chatterino (twitch-badges.json) void loadLocalBadges(); +private: void loaded(); void loadEmoteImage(const QString &name, const ImagePtr &image, BadgeIconCallback &&callback); diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 48bc1ee19..93ac4519c 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -462,6 +462,14 @@ void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward) } } +void TwitchChannel::addKnownChannelPointReward(const ChannelPointReward &reward) +{ + assert(getApp()->isTest()); + + auto channelPointRewards = this->channelPointRewards_.access(); + channelPointRewards->try_emplace(reward.id, reward); +} + bool TwitchChannel::isChannelPointRewardKnown(const QString &rewardId) { const auto &pointRewards = this->channelPointRewards_.accessConst(); @@ -1560,7 +1568,7 @@ void TwitchChannel::refreshBadges() getHelix()->getChannelBadges( this->roomId(), // successCallback - [this, weak = weakOf(this)](auto channelBadges) { + [this, weak = weakOf(this)](const auto &channelBadges) { auto shared = weak.lock(); if (!shared) { @@ -1568,31 +1576,7 @@ void TwitchChannel::refreshBadges() return; } - auto badgeSets = this->badgeSets_.access(); - - for (const auto &badgeSet : channelBadges.badgeSets) - { - const auto &setID = badgeSet.setID; - for (const auto &version : badgeSet.versions) - { - auto emote = Emote{ - .name = EmoteName{}, - .images = - ImageSet{ - Image::fromUrl(version.imageURL1x, 1, - BASE_BADGE_SIZE), - Image::fromUrl(version.imageURL2x, .5, - BASE_BADGE_SIZE * 2), - Image::fromUrl(version.imageURL4x, .25, - BASE_BADGE_SIZE * 4), - }, - .tooltip = Tooltip{version.title}, - .homePage = version.clickURL, - }; - (*badgeSets)[setID][version.id] = - std::make_shared(emote); - } - } + this->addTwitchBadgeSets(channelBadges); }, // failureCallback [this, weak = weakOf(this)](auto error, auto message) { @@ -1623,6 +1607,33 @@ void TwitchChannel::refreshBadges() }); } +void TwitchChannel::addTwitchBadgeSets(const HelixChannelBadges &channelBadges) +{ + auto badgeSets = this->badgeSets_.access(); + + for (const auto &badgeSet : channelBadges.badgeSets) + { + const auto &setID = badgeSet.setID; + for (const auto &version : badgeSet.versions) + { + auto emote = Emote{ + .name = EmoteName{}, + .images = + ImageSet{ + Image::fromUrl(version.imageURL1x, 1, BASE_BADGE_SIZE), + Image::fromUrl(version.imageURL2x, .5, + BASE_BADGE_SIZE * 2), + Image::fromUrl(version.imageURL4x, .25, + BASE_BADGE_SIZE * 4), + }, + .tooltip = Tooltip{version.title}, + .homePage = version.clickURL, + }; + (*badgeSets)[setID][version.id] = std::make_shared(emote); + } + } +} + void TwitchChannel::refreshCheerEmotes() { getHelix()->getCheermotes( @@ -1635,74 +1646,75 @@ void TwitchChannel::refreshCheerEmotes() return; } - std::vector emoteSets; - - for (const auto &set : cheermoteSets) - { - auto cheerEmoteSet = CheerEmoteSet(); - cheerEmoteSet.regex = QRegularExpression( - "^" + set.prefix + "([1-9][0-9]*)$", - QRegularExpression::CaseInsensitiveOption); - - for (const auto &tier : set.tiers) - { - CheerEmote cheerEmote; - - cheerEmote.color = QColor(tier.color); - cheerEmote.minBits = tier.minBits; - cheerEmote.regex = cheerEmoteSet.regex; - - // TODO(pajlada): We currently hardcode dark here :| - // We will continue to do so for now since we haven't had to - // solve that anywhere else - - // Combine the prefix (e.g. BibleThump) with the tier (1, 100 etc.) - auto emoteTooltip = - set.prefix + tier.id + "
Twitch Cheer Emote"; - auto makeImageSet = [](const HelixCheermoteImage &image) { - return ImageSet{ - Image::fromUrl(image.imageURL1x, 1.0, - BASE_BADGE_SIZE), - Image::fromUrl(image.imageURL2x, 0.5, - BASE_BADGE_SIZE * 2), - Image::fromUrl(image.imageURL4x, 0.25, - BASE_BADGE_SIZE * 4), - }; - }; - cheerEmote.animatedEmote = std::make_shared(Emote{ - .name = EmoteName{"cheer emote"}, - .images = makeImageSet(tier.darkAnimated), - .tooltip = Tooltip{emoteTooltip}, - .homePage = Url{}, - }); - cheerEmote.staticEmote = std::make_shared(Emote{ - .name = EmoteName{"cheer emote"}, - .images = makeImageSet(tier.darkStatic), - .tooltip = Tooltip{emoteTooltip}, - .homePage = Url{}, - }); - - cheerEmoteSet.cheerEmotes.emplace_back( - std::move(cheerEmote)); - } - - // Sort cheermotes by cost - std::sort(cheerEmoteSet.cheerEmotes.begin(), - cheerEmoteSet.cheerEmotes.end(), - [](const auto &lhs, const auto &rhs) { - return lhs.minBits > rhs.minBits; - }); - - emoteSets.emplace_back(std::move(cheerEmoteSet)); - } - - *this->cheerEmoteSets_.access() = std::move(emoteSets); + this->setCheerEmoteSets(cheermoteSets); }, [] { // Failure }); } +void TwitchChannel::setCheerEmoteSets( + const std::vector &cheermoteSets) +{ + std::vector emoteSets; + + for (const auto &set : cheermoteSets) + { + auto cheerEmoteSet = CheerEmoteSet(); + cheerEmoteSet.regex = + QRegularExpression("^" + set.prefix + "([1-9][0-9]*)$", + QRegularExpression::CaseInsensitiveOption); + + for (const auto &tier : set.tiers) + { + CheerEmote cheerEmote; + + cheerEmote.color = QColor(tier.color); + cheerEmote.minBits = tier.minBits; + cheerEmote.regex = cheerEmoteSet.regex; + + // TODO(pajlada): We currently hardcode dark here :| + // We will continue to do so for now since we haven't had to + // solve that anywhere else + + // Combine the prefix (e.g. BibleThump) with the tier (1, 100 etc.) + auto emoteTooltip = set.prefix + tier.id + "
Twitch Cheer Emote"; + auto makeImageSet = [](const HelixCheermoteImage &image) { + return ImageSet{ + Image::fromUrl(image.imageURL1x, 1.0, BASE_BADGE_SIZE), + Image::fromUrl(image.imageURL2x, 0.5, BASE_BADGE_SIZE * 2), + Image::fromUrl(image.imageURL4x, 0.25, BASE_BADGE_SIZE * 4), + }; + }; + cheerEmote.animatedEmote = std::make_shared(Emote{ + .name = EmoteName{u"cheer emote"_s}, + .images = makeImageSet(tier.darkAnimated), + .tooltip = Tooltip{emoteTooltip}, + .homePage = Url{}, + }); + cheerEmote.staticEmote = std::make_shared(Emote{ + .name = EmoteName{u"cheer emote"_s}, + .images = makeImageSet(tier.darkStatic), + .tooltip = Tooltip{emoteTooltip}, + .homePage = Url{}, + }); + + cheerEmoteSet.cheerEmotes.emplace_back(std::move(cheerEmote)); + } + + // Sort cheermotes by cost + std::sort(cheerEmoteSet.cheerEmotes.begin(), + cheerEmoteSet.cheerEmotes.end(), + [](const auto &lhs, const auto &rhs) { + return lhs.minBits > rhs.minBits; + }); + + emoteSets.emplace_back(std::move(cheerEmoteSet)); + } + + *this->cheerEmoteSets_.access() = std::move(emoteSets); +} + void TwitchChannel::createClip() { if (!this->isLive()) @@ -1859,6 +1871,12 @@ std::vector TwitchChannel::ffzChannelBadges( return badges; } +void TwitchChannel::setFfzChannelBadges(FfzChannelBadgeMap map) +{ + this->tgFfzChannelBadges_.guard(); + this->ffzChannelBadges_ = std::move(map); +} + std::optional TwitchChannel::ffzCustomModBadge() const { return this->ffzCustomModBadge_.get(); @@ -1869,6 +1887,16 @@ std::optional TwitchChannel::ffzCustomVipBadge() const return this->ffzCustomVipBadge_.get(); } +void TwitchChannel::setFfzCustomModBadge(std::optional badge) +{ + this->ffzCustomModBadge_.set(std::move(badge)); +} + +void TwitchChannel::setFfzCustomVipBadge(std::optional badge) +{ + this->ffzCustomVipBadge_.set(std::move(badge)); +} + std::optional TwitchChannel::cheerEmote(const QString &string) const { auto sets = this->cheerEmoteSets_.access(); diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index fd2edaf79..802da8112 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -25,6 +25,8 @@ #include #include +class TestMessageBuilderP; + namespace chatterino { enum class HighlightState; @@ -51,6 +53,9 @@ struct ChannelPointReward; class MessageThread; struct CheerEmoteSet; struct HelixStream; +struct HelixCheermoteSet; +struct HelixGlobalBadges; +using HelixChannelBadges = HelixGlobalBadges; class TwitchIrcServer; @@ -195,9 +200,15 @@ public: * Returns a list of channel-specific FrankerFaceZ badges for the given user */ std::vector ffzChannelBadges(const QString &userID) const; + void setFfzChannelBadges(FfzChannelBadgeMap map); + void setFfzCustomModBadge(std::optional badge); + void setFfzCustomVipBadge(std::optional badge); + + void addTwitchBadgeSets(const HelixChannelBadges &channelBadges); // Cheers std::optional cheerEmote(const QString &string) const; + void setCheerEmoteSets(const std::vector &cheermoteSets); // Replies /** @@ -243,6 +254,10 @@ public: * This will look at queued up partial messages, and if one is found it will add the queued up partial messages fully hydrated. **/ void addChannelPointReward(const ChannelPointReward &reward); + /// Adds @a reward to the known rewards + /// + /// Unlike in #addChannelPointReward(), no message will be sent. + void addKnownChannelPointReward(const ChannelPointReward &reward); bool isChannelPointRewardKnown(const QString &rewardId); std::optional channelPointReward( const QString &rewardId) const; @@ -449,6 +464,7 @@ private: friend class MessageBuilder; friend class IrcMessageHandler; friend class Commands_E2E_Test; + friend class ::TestMessageBuilderP; }; } // namespace chatterino diff --git a/src/util/Helpers.cpp b/src/util/Helpers.cpp index e32c1c7ff..4df84739a 100644 --- a/src/util/Helpers.cpp +++ b/src/util/Helpers.cpp @@ -1,5 +1,6 @@ #include "util/Helpers.hpp" +#include "Application.hpp" #include "providers/twitch/TwitchCommon.hpp" #include @@ -301,4 +302,16 @@ QString unescapeZeroWidthJoiner(QString escaped) return escaped; } +QLocale getSystemLocale() +{ +#ifdef CHATTERINO_WITH_TESTS + if (getApp()->isTest()) + { + return {QLocale::English}; + } +#endif + + return QLocale::system(); +} + } // namespace chatterino diff --git a/src/util/Helpers.hpp b/src/util/Helpers.hpp index 340ad5fcd..5ea221f29 100644 --- a/src/util/Helpers.hpp +++ b/src/util/Helpers.hpp @@ -189,4 +189,6 @@ constexpr std::optional> makeConditionedOptional(bool condition, /// a ZWJ. See also: https://github.com/Chatterino/chatterino2/issues/3384. QString unescapeZeroWidthJoiner(QString escaped); +QLocale getSystemLocale(); + } // namespace chatterino diff --git a/src/util/IrcHelpers.cpp b/src/util/IrcHelpers.cpp new file mode 100644 index 000000000..5096594aa --- /dev/null +++ b/src/util/IrcHelpers.cpp @@ -0,0 +1,72 @@ +#include "util/IrcHelpers.hpp" + +#include "Application.hpp" + +namespace { + +using namespace chatterino; + +QDateTime calculateMessageTimeBase(const Communi::IrcMessage *message) +{ + // Check if message is from recent-messages API + if (message->tags().contains("historical")) + { + bool customReceived = false; + auto ts = + message->tags().value("rm-received-ts").toLongLong(&customReceived); + if (!customReceived) + { + ts = message->tags().value("tmi-sent-ts").toLongLong(); + } + + return QDateTime::fromMSecsSinceEpoch(ts); + } + + // If present, handle tmi-sent-ts tag and use it as timestamp + if (message->tags().contains("tmi-sent-ts")) + { + auto ts = message->tags().value("tmi-sent-ts").toLongLong(); + return QDateTime::fromMSecsSinceEpoch(ts); + } + + // Some IRC Servers might have server-time tag containing UTC date in ISO format, use it as timestamp + // See: https://ircv3.net/irc/#server-time + if (message->tags().contains("time")) + { + QString timedate = message->tags().value("time").toString(); + + auto date = QDateTime::fromString(timedate, Qt::ISODate); + date.setTimeZone(QTimeZone::utc()); + return date.toLocalTime(); + } + + // Fallback to current time +#ifdef CHATTERINO_WITH_TESTS + if (getApp()->isTest()) + { + return QDateTime::fromMSecsSinceEpoch(0, QTimeZone::utc()); + } +#endif + + return QDateTime::currentDateTime(); +} + +} // namespace + +namespace chatterino { + +QDateTime calculateMessageTime(const Communi::IrcMessage *message) +{ + auto dt = calculateMessageTimeBase(message); + +#ifdef CHATTERINO_WITH_TESTS + if (getApp()->isTest()) + { + return dt.toUTC(); + } +#endif + + return dt; +} + +} // namespace chatterino diff --git a/src/util/IrcHelpers.hpp b/src/util/IrcHelpers.hpp index 52691b350..f7941be08 100644 --- a/src/util/IrcHelpers.hpp +++ b/src/util/IrcHelpers.hpp @@ -59,43 +59,7 @@ inline QString parseTagString(const QString &input) return output; } -inline QDateTime calculateMessageTime(const Communi::IrcMessage *message) -{ - // Check if message is from recent-messages API - if (message->tags().contains("historical")) - { - bool customReceived = false; - auto ts = - message->tags().value("rm-received-ts").toLongLong(&customReceived); - if (!customReceived) - { - ts = message->tags().value("tmi-sent-ts").toLongLong(); - } - - return QDateTime::fromMSecsSinceEpoch(ts); - } - - // If present, handle tmi-sent-ts tag and use it as timestamp - if (message->tags().contains("tmi-sent-ts")) - { - auto ts = message->tags().value("tmi-sent-ts").toLongLong(); - return QDateTime::fromMSecsSinceEpoch(ts); - } - - // Some IRC Servers might have server-time tag containing UTC date in ISO format, use it as timestamp - // See: https://ircv3.net/irc/#server-time - if (message->tags().contains("time")) - { - QString timedate = message->tags().value("time").toString(); - - auto date = QDateTime::fromString(timedate, Qt::ISODate); - date.setTimeZone(QTimeZone::utc()); - return date.toLocalTime(); - } - - // Fallback to current time - return QDateTime::currentDateTime(); -} +QDateTime calculateMessageTime(const Communi::IrcMessage *message); // "foo/bar/baz,tri/hard" can be a valid badge-info tag // In that case, valid map content should be 'split by slash' only once: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 41f90b1ff..36e7e31d8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,6 +48,8 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp ${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.cpp ${CMAKE_CURRENT_LIST_DIR}/src/CancellationToken.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.hpp # Add your new file above this line! ) @@ -59,6 +61,8 @@ target_link_libraries(${PROJECT_NAME} PRIVATE chatterino-mocks) target_link_libraries(${PROJECT_NAME} PRIVATE gtest gmock) +target_include_directories(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/src") + set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" diff --git a/tests/snapshots/MessageBuilder/IRC/action.json b/tests/snapshots/MessageBuilder/IRC/action.json new file mode 100644 index 000000000..f70f3ed47 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/action.json @@ -0,0 +1,191 @@ +{ + "input": "@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=90ef1e46-8baa-4bf2-9c54-272f39d6fa11;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662206235860;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :\u0001ACTION Kappa\u0001", + "output": [ + { + "badgeInfos": { + "subscriber": "80" + }, + "badges": [ + "broadcaster", + "subscriber", + "partner" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "pajlada", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "11:57" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "11:57:15", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/3" + }, + "name": "", + "tooltip": "Broadcaster" + }, + "flags": "BadgeChannelAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Broadcaster", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/tb-1", + "2x": "https://chatterino.com/tb-2", + "3x": "https://chatterino.com/tb-3" + }, + "name": "", + "tooltip": "Subscriber" + }, + "flags": "BadgeSubscription", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Subscriber (Tier 3, 80 months)", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "homePage": "https://blog.twitch.tv/2017/04/24/the-verified-badge-is-here-13381bc05735", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/d12a2e27-16f6-41d0-ab77-b780518f00a3/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/d12a2e27-16f6-41d0-ab77-b780518f00a3/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/d12a2e27-16f6-41d0-ab77-b780518f00a3/3" + }, + "name": "", + "tooltip": "Verified" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Verified", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffcc44ff", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "pajlada" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "pajlada" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "#ffcc44ff", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "90ef1e46-8baa-4bf2-9c54-272f39d6fa11" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|Action", + "id": "90ef1e46-8baa-4bf2-9c54-272f39d6fa11", + "localizedName": "", + "loginName": "pajlada", + "messageText": "Kappa", + "searchText": "pajlada pajlada: Kappa ", + "serverReceivedTime": "2022-09-03T11:57:15Z", + "timeoutUser": "", + "usernameColor": "#ffcc44ff" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/all-usernames.json b/tests/snapshots/MessageBuilder/IRC/all-usernames.json new file mode 100644 index 000000000..d74b64bd4 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/all-usernames.json @@ -0,0 +1,315 @@ +{ + "input": "@tmi-sent-ts=1726689518974;subscriber=1;id=438b85cc-fa67-4c03-bc38-c4ec2527822c;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@TwitchDev TwitchDev UserColor @UserColor UserColor! UserColorKappa UserChatter usercolor UserColor2 @UserColor2 ?!", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "19:58" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "19:58:38", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "fallbackColor": "Text", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "Text", + "userLoginName": "TwitchDev", + "words": [ + "@TwitchDev" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "TwitchDev" + ] + }, + { + "color": "Text", + "fallbackColor": "Text", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "#ff010203", + "userLoginName": "UserColor", + "words": [ + "UserColor" + ] + }, + { + "color": "Text", + "fallbackColor": "Text", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "#ff010203", + "userLoginName": "UserColor", + "words": [ + "@UserColor" + ] + }, + { + "color": "Text", + "fallbackColor": "Text", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": false, + "type": "MentionElement", + "userColor": "#ff010203", + "userLoginName": "UserColor", + "words": [ + "UserColor" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "!" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "UserColorKappa" + ] + }, + { + "color": "Text", + "fallbackColor": "Text", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "Text", + "userLoginName": "UserChatter", + "words": [ + "UserChatter" + ] + }, + { + "color": "Text", + "fallbackColor": "Text", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "#ff010203", + "userLoginName": "usercolor", + "words": [ + "usercolor" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "UserColor2" + ] + }, + { + "color": "Text", + "fallbackColor": "Text", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "#ff050607", + "userLoginName": "UserColor2", + "words": [ + "@UserColor2" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "?!" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "438b85cc-fa67-4c03-bc38-c4ec2527822c" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "438b85cc-fa67-4c03-bc38-c4ec2527822c", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "@TwitchDev TwitchDev UserColor @UserColor UserColor! UserColorKappa UserChatter usercolor UserColor2 @UserColor2 ?!", + "searchText": "nerixyz nerixyz: @TwitchDev TwitchDev UserColor @UserColor UserColor! UserColorKappa UserChatter usercolor UserColor2 @UserColor2 ?! ", + "serverReceivedTime": "2024-09-18T19:58:38Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "settings": { + "appearance": { + "messages": { + "findAllUsernames": true + } + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/bad-emotes.json b/tests/snapshots/MessageBuilder/IRC/bad-emotes.json new file mode 100644 index 000000000..33314c1e8 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/bad-emotes.json @@ -0,0 +1,159 @@ +{ + "input": "@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=25:04;first-msg=0;flags=;id=7be87072-bf24-4fa3-b3df-0ea6fa5f1474;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201102276;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Kappa", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "no_audio" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "Mm2PL", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:31" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:31:42", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/3" + }, + "name": "", + "tooltip": "Watching without audio" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Watching without audio", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffdaa521", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "Mm2PL" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Mm2PL:" + ] + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/Kappa", + "images": { + "1x": "https://chatterino.com/Kappa.png" + }, + "name": "Kappa", + "tooltip": "Kappa Tooltip" + }, + "flags": "BttvEmoteImage|BttvEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "7be87072-bf24-4fa3-b3df-0ea6fa5f1474" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "7be87072-bf24-4fa3-b3df-0ea6fa5f1474", + "localizedName": "", + "loginName": "mm2pl", + "messageText": "Kappa", + "searchText": "mm2pl mm2pl: Kappa ", + "serverReceivedTime": "2022-09-03T10:31:42Z", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/bad-emotes2.json b/tests/snapshots/MessageBuilder/IRC/bad-emotes2.json new file mode 100644 index 000000000..4455bf818 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/bad-emotes2.json @@ -0,0 +1,159 @@ +{ + "input": "@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=25:4-0;first-msg=0;flags=;id=7be87072-bf24-4fa3-b3df-0ea6fa5f1474;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201102276;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Kappa", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "no_audio" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "Mm2PL", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:31" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:31:42", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/3" + }, + "name": "", + "tooltip": "Watching without audio" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Watching without audio", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffdaa521", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "Mm2PL" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Mm2PL:" + ] + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/Kappa", + "images": { + "1x": "https://chatterino.com/Kappa.png" + }, + "name": "Kappa", + "tooltip": "Kappa Tooltip" + }, + "flags": "BttvEmoteImage|BttvEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "7be87072-bf24-4fa3-b3df-0ea6fa5f1474" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "7be87072-bf24-4fa3-b3df-0ea6fa5f1474", + "localizedName": "", + "loginName": "mm2pl", + "messageText": "Kappa", + "searchText": "mm2pl mm2pl: Kappa ", + "serverReceivedTime": "2022-09-03T10:31:42Z", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/bad-emotes3.json b/tests/snapshots/MessageBuilder/IRC/bad-emotes3.json new file mode 100644 index 000000000..aaefb0fcf --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/bad-emotes3.json @@ -0,0 +1,135 @@ +{ + "input": "@tmi-sent-ts=1662201102276;emotes=84608:0-15 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "pajlada", + "count": 1, + "displayName": "", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:31" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:31:42", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ff999999", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "test:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "bar" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "", + "localizedName": "", + "loginName": "test", + "messageText": "foo bar", + "searchText": "test test: foo bar ", + "serverReceivedTime": "2022-09-03T10:31:42Z", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/bad-emotes4.json b/tests/snapshots/MessageBuilder/IRC/bad-emotes4.json new file mode 100644 index 000000000..e70385c6f --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/bad-emotes4.json @@ -0,0 +1,135 @@ +{ + "input": "@tmi-sent-ts=1662201102276;emotes=84608:9-10 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "pajlada", + "count": 1, + "displayName": "", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:31" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:31:42", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ff999999", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "test:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "bar" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "", + "localizedName": "", + "loginName": "test", + "messageText": "foo bar", + "searchText": "test test: foo bar ", + "serverReceivedTime": "2022-09-03T10:31:42Z", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/badges-invalid.json b/tests/snapshots/MessageBuilder/IRC/badges-invalid.json new file mode 100644 index 000000000..3680b1980 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/badges-invalid.json @@ -0,0 +1,228 @@ +{ + "input": "@tmi-sent-ts=1726764056444;subscriber=1;id=546c42a6-21d0-4f3d-a6a0-c77f78d7b131;room-id=11148817;user-id=123456;display-name=badgy;badges=subscriber24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :badgy!badgy@badgy.tmi.twitch.tv PRIVMSG #pajlada :badge", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + ], + "channelName": "pajlada", + "count": 1, + "displayName": "badgy", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "16:40" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "16:40:56", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/Chatterino.png" + }, + "name": "", + "tooltip": "Chatterino badge" + }, + "flags": "BadgeChatterino", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Chatterino badge", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#0c090a0b", + "emote": { + "images": { + "1x": "https://chatterino.com/FFZ1.png" + }, + "name": "", + "tooltip": "FFZ1 badge" + }, + "flags": "BadgeFfz", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "FFZ1 badge", + "trailingSpace": true, + "type": "FfzBadgeElement" + }, + { + "color": "#100d0e0f", + "emote": { + "images": { + "1x": "https://chatterino.com/FFZ2.png" + }, + "name": "", + "tooltip": "FFZ2 badge" + }, + "flags": "BadgeFfz", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "FFZ2 badge", + "trailingSpace": true, + "type": "FfzBadgeElement" + }, + { + "color": "#14111213", + "emote": { + "images": { + "1x": "https://chatterino.com/FFZ2.png" + }, + "name": "", + "tooltip": "FFZ2 badge" + }, + "flags": "BadgeFfz", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "FFZ2 badge", + "trailingSpace": true, + "type": "FfzBadgeElement" + }, + { + "color": "#18151617", + "emote": { + "images": { + "1x": "https://chatterino.com/FFZ2.png" + }, + "name": "", + "tooltip": "FFZ2 badge" + }, + "flags": "BadgeFfz", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "FFZ2 badge", + "trailingSpace": true, + "type": "FfzBadgeElement" + }, + { + "emote": { + "id": "1", + "images": { + "1x": "https://chatterino.com/7tv//1x" + }, + "name": "", + "tooltip": "7TV badge" + }, + "flags": "BadgeSevenTV", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "7TV badge", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "badgy" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "badgy:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "badge" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "546c42a6-21d0-4f3d-a6a0-c77f78d7b131" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "546c42a6-21d0-4f3d-a6a0-c77f78d7b131", + "localizedName": "", + "loginName": "badgy", + "messageText": "badge", + "searchText": "badgy badgy: badge ", + "serverReceivedTime": "2024-09-19T16:40:56Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/badges.json b/tests/snapshots/MessageBuilder/IRC/badges.json new file mode 100644 index 000000000..313cf600c --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/badges.json @@ -0,0 +1,229 @@ +{ + "input": "@tmi-sent-ts=1726764056444;subscriber=1;id=546c42a6-21d0-4f3d-a6a0-c77f78d7b131;room-id=11148817;user-id=123456;display-name=badgy;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :badgy!badgy@badgy.tmi.twitch.tv PRIVMSG #pajlada :badge", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "badgy", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "16:40" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "16:40:56", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/Chatterino.png" + }, + "name": "", + "tooltip": "Chatterino badge" + }, + "flags": "BadgeChatterino", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Chatterino badge", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#0c090a0b", + "emote": { + "images": { + "1x": "https://chatterino.com/FFZ1.png" + }, + "name": "", + "tooltip": "FFZ1 badge" + }, + "flags": "BadgeFfz", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "FFZ1 badge", + "trailingSpace": true, + "type": "FfzBadgeElement" + }, + { + "color": "#100d0e0f", + "emote": { + "images": { + "1x": "https://chatterino.com/FFZ2.png" + }, + "name": "", + "tooltip": "FFZ2 badge" + }, + "flags": "BadgeFfz", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "FFZ2 badge", + "trailingSpace": true, + "type": "FfzBadgeElement" + }, + { + "color": "#14111213", + "emote": { + "images": { + "1x": "https://chatterino.com/FFZ2.png" + }, + "name": "", + "tooltip": "FFZ2 badge" + }, + "flags": "BadgeFfz", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "FFZ2 badge", + "trailingSpace": true, + "type": "FfzBadgeElement" + }, + { + "color": "#18151617", + "emote": { + "images": { + "1x": "https://chatterino.com/FFZ2.png" + }, + "name": "", + "tooltip": "FFZ2 badge" + }, + "flags": "BadgeFfz", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "FFZ2 badge", + "trailingSpace": true, + "type": "FfzBadgeElement" + }, + { + "emote": { + "id": "1", + "images": { + "1x": "https://chatterino.com/7tv//1x" + }, + "name": "", + "tooltip": "7TV badge" + }, + "flags": "BadgeSevenTV", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "7TV badge", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "badgy" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "badgy:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "badge" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "546c42a6-21d0-4f3d-a6a0-c77f78d7b131" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "546c42a6-21d0-4f3d-a6a0-c77f78d7b131", + "localizedName": "", + "loginName": "badgy", + "messageText": "badge", + "searchText": "badgy badgy: badge ", + "serverReceivedTime": "2024-09-19T16:40:56Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/blocked-user.json b/tests/snapshots/MessageBuilder/IRC/blocked-user.json new file mode 100644 index 000000000..ee23a860f --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/blocked-user.json @@ -0,0 +1,5 @@ +{ + "input": "@tmi-sent-ts=1726692855226;subscriber=1;id=ff82c584-5d20-459f-b2d5-dcacbb693559;room-id=11148817;user-id=12345;display-name=blocked;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :blocked!blocked@blocked.tmi.twitch.tv PRIVMSG #pajlada :a", + "output": [ + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/cheer1.json b/tests/snapshots/MessageBuilder/IRC/cheer1.json new file mode 100644 index 000000000..d85bf8844 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/cheer1.json @@ -0,0 +1,596 @@ +{ + "input": "@badge-info=;badges=bits-leader/3;bits=5;color=;display-name=slyckity;emotes=;flags=;id=fd6c5507-3a4e-4d24-8f6e-fadf07f520d3;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567824273752;turbo=0;user-id=143114011;user-type= :slyckity!slyckity@slyckity.tmi.twitch.tv PRIVMSG #pajlada :Cheer1 a Cheer1 Cheer1 Cheer1 Cheer1 b c", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "bits-leader" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "slyckity", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2:44" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "02:44:33", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "homePage": "https://bits.twitch.tv", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/f1d2aab6-b647-47af-965b-84909cf303aa/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/f1d2aab6-b647-47af-965b-84909cf303aa/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/f1d2aab6-b647-47af-965b-84909cf303aa/3" + }, + "name": "", + "tooltip": "Bits Leader 3" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Bits Leader 3", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff0000ff", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "slyckity" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "slyckity:" + ] + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.png", + "2x": "https://chatterino.com/bits/2.png", + "3x": "https://chatterino.com/bits/4.png" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsStatic", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.gif", + "2x": "https://chatterino.com/bits/2.gif", + "3x": "https://chatterino.com/bits/4.gif" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsAnimated", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "#ff979797", + "flags": "BitsAmount", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.png", + "2x": "https://chatterino.com/bits/2.png", + "3x": "https://chatterino.com/bits/4.png" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsStatic", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.gif", + "2x": "https://chatterino.com/bits/2.gif", + "3x": "https://chatterino.com/bits/4.gif" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsAnimated", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "#ff979797", + "flags": "BitsAmount", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.png", + "2x": "https://chatterino.com/bits/2.png", + "3x": "https://chatterino.com/bits/4.png" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsStatic", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.gif", + "2x": "https://chatterino.com/bits/2.gif", + "3x": "https://chatterino.com/bits/4.gif" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsAnimated", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "#ff979797", + "flags": "BitsAmount", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.png", + "2x": "https://chatterino.com/bits/2.png", + "3x": "https://chatterino.com/bits/4.png" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsStatic", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.gif", + "2x": "https://chatterino.com/bits/2.gif", + "3x": "https://chatterino.com/bits/4.gif" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsAnimated", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "#ff979797", + "flags": "BitsAmount", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.png", + "2x": "https://chatterino.com/bits/2.png", + "3x": "https://chatterino.com/bits/4.png" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsStatic", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.gif", + "2x": "https://chatterino.com/bits/2.gif", + "3x": "https://chatterino.com/bits/4.gif" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsAnimated", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "#ff979797", + "flags": "BitsAmount", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "b" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "c" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "fd6c5507-3a4e-4d24-8f6e-fadf07f520d3" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|CheerMessage", + "id": "fd6c5507-3a4e-4d24-8f6e-fadf07f520d3", + "localizedName": "", + "loginName": "slyckity", + "messageText": "Cheer1 a Cheer1 Cheer1 Cheer1 Cheer1 b c", + "searchText": "slyckity slyckity: Cheer1 a Cheer1 Cheer1 Cheer1 Cheer1 b c ", + "serverReceivedTime": "2019-09-07T02:44:33Z", + "timeoutUser": "", + "usernameColor": "#ff0000ff" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/cheer2.json b/tests/snapshots/MessageBuilder/IRC/cheer2.json new file mode 100644 index 000000000..779645fbc --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/cheer2.json @@ -0,0 +1,261 @@ +{ + "input": "@badge-info=;badges=bits-leader/3;bits=5;color=;display-name=slyckity;emotes=;flags=;id=fd6c5507-3a4e-4d24-8f6e-fadf07f520d3;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567824273752;turbo=0;user-id=143114011;user-type= :slyckity!slyckity@slyckity.tmi.twitch.tv PRIVMSG #pajlada :Cheer1 a Cheer1 Cheer1 Cheer1 Cheer1 b c", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "bits-leader" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "slyckity", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2:44" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "02:44:33", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "homePage": "https://bits.twitch.tv", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/f1d2aab6-b647-47af-965b-84909cf303aa/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/f1d2aab6-b647-47af-965b-84909cf303aa/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/f1d2aab6-b647-47af-965b-84909cf303aa/3" + }, + "name": "", + "tooltip": "Bits Leader 3" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Bits Leader 3", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff0000ff", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "slyckity" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "slyckity:" + ] + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.png", + "2x": "https://chatterino.com/bits/2.png", + "3x": "https://chatterino.com/bits/4.png" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsStatic", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.gif", + "2x": "https://chatterino.com/bits/2.gif", + "3x": "https://chatterino.com/bits/4.gif" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsAnimated", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "#ff979797", + "flags": "BitsAmount", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "5" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "b" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "c" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "fd6c5507-3a4e-4d24-8f6e-fadf07f520d3" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|CheerMessage", + "id": "fd6c5507-3a4e-4d24-8f6e-fadf07f520d3", + "localizedName": "", + "loginName": "slyckity", + "messageText": "Cheer1 a Cheer1 Cheer1 Cheer1 Cheer1 b c", + "searchText": "slyckity slyckity: Cheer1 a Cheer1 Cheer1 Cheer1 Cheer1 b c ", + "serverReceivedTime": "2019-09-07T02:44:33Z", + "timeoutUser": "", + "usernameColor": "#ff0000ff" + } + ], + "settings": { + "emotes": { + "stackBits": true + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/cheer3.json b/tests/snapshots/MessageBuilder/IRC/cheer3.json new file mode 100644 index 000000000..33b474a4a --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/cheer3.json @@ -0,0 +1,211 @@ +{ + "input": "@badge-info=;badges=bits-leader/3;bits=5;color=;display-name=slyckity;emotes=;flags=;id=fd6c5507-3a4e-4d24-8f6e-fadf07f520d3;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567824273752;turbo=0;user-id=143114011;user-type= :slyckity!slyckity@slyckity.tmi.twitch.tv PRIVMSG #pajlada :Cheer100", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "bits-leader" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "slyckity", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2:44" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "02:44:33", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "homePage": "https://bits.twitch.tv", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/f1d2aab6-b647-47af-965b-84909cf303aa/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/f1d2aab6-b647-47af-965b-84909cf303aa/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/f1d2aab6-b647-47af-965b-84909cf303aa/3" + }, + "name": "", + "tooltip": "Bits Leader 3" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Bits Leader 3", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff0000ff", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "slyckity" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "slyckity:" + ] + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.png", + "2x": "https://chatterino.com/bits/2.png", + "3x": "https://chatterino.com/bits/4.png" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsStatic", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/bits/1.gif", + "2x": "https://chatterino.com/bits/2.gif", + "3x": "https://chatterino.com/bits/4.gif" + }, + "name": "cheer emote", + "tooltip": "Cheer1
Twitch Cheer Emote" + }, + "flags": "BitsAnimated", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cheer", + "emote" + ] + }, + "tooltip": "Cheer1
Twitch Cheer Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "#ff979797", + "flags": "BitsAmount", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "5" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "fd6c5507-3a4e-4d24-8f6e-fadf07f520d3" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|CheerMessage", + "id": "fd6c5507-3a4e-4d24-8f6e-fadf07f520d3", + "localizedName": "", + "loginName": "slyckity", + "messageText": "Cheer100", + "searchText": "slyckity slyckity: Cheer100 ", + "serverReceivedTime": "2019-09-07T02:44:33Z", + "timeoutUser": "", + "usernameColor": "#ff0000ff" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/cheer4.json b/tests/snapshots/MessageBuilder/IRC/cheer4.json new file mode 100644 index 000000000..24d5b9ba3 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/cheer4.json @@ -0,0 +1,141 @@ +{ + "input": "@badge-info=;badges=bits/1;bits=10;color=#00FF7F;display-name=EXDE_HUN;emotes=;flags=;id=60d8835b-23fa-418c-96ca-5874e5d5e8ba;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1566654664248;turbo=0;user-id=129793695;user-type= :exde_hun!exde_hun@exde_hun.tmi.twitch.tv PRIVMSG #pajlada :PogChamp10", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "bits" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "EXDE_HUN", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "13:51" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "13:51:04", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "homePage": "https://bits.twitch.tv", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/73b5c3fb-24f9-4a82-a852-2f475b59411c/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/73b5c3fb-24f9-4a82-a852-2f475b59411c/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/73b5c3fb-24f9-4a82-a852-2f475b59411c/3" + }, + "name": "", + "tooltip": "cheer 1" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Twitch cheer 1", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff00ff7f", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "EXDE_HUN" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "EXDE_HUN:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "PogChamp10" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "60d8835b-23fa-418c-96ca-5874e5d5e8ba" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|CheerMessage", + "id": "60d8835b-23fa-418c-96ca-5874e5d5e8ba", + "localizedName": "", + "loginName": "exde_hun", + "messageText": "PogChamp10", + "searchText": "exde_hun exde_hun: PogChamp10 ", + "serverReceivedTime": "2019-08-24T13:51:04Z", + "timeoutUser": "", + "usernameColor": "#ff00ff7f" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/custom-mod.json b/tests/snapshots/MessageBuilder/IRC/custom-mod.json new file mode 100644 index 000000000..825b0918f --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/custom-mod.json @@ -0,0 +1,133 @@ +{ + "input": "@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=;flags=;id=97c28382-e8d2-45a0-bb5d-2305fc4ef139;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922036771;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :foo", + "output": [ + { + "badgeInfos": { + "subscriber": "34" + }, + "badges": [ + "moderator", + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "testaccount_420", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:47" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:47:16", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/ffz-mod1x.png" + }, + "name": "", + "tooltip": "Moderator" + }, + "flags": "BadgeChannelAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Moderator", + "trailingSpace": true, + "type": "ModBadgeElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "testaccount_420" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "testaccount_420(테스트계정420):" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "97c28382-e8d2-45a0-bb5d-2305fc4ef139" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "97c28382-e8d2-45a0-bb5d-2305fc4ef139", + "localizedName": "테스트계정420", + "loginName": "testaccount_420", + "messageText": "foo", + "searchText": "testaccount_420(테스트계정420) 테스트계정420 testaccount_420: foo ", + "serverReceivedTime": "2020-05-31T10:47:16Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "params": { + "ffzCustomModBadge": true + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/custom-vip.json b/tests/snapshots/MessageBuilder/IRC/custom-vip.json new file mode 100644 index 000000000..d66cd81b8 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/custom-vip.json @@ -0,0 +1,143 @@ +{ + "input": "@tmi-sent-ts=1726920321214;subscriber=1;vip=1;id=97bb0dfb-a35f-446d-8634-7522d8ef73ed;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=vip/1,subscriber/48;badge-info=subscriber/64;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :a", + "output": [ + { + "badgeInfos": { + "subscriber": "64" + }, + "badges": [ + "vip", + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "12:05" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "12:05:21", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/ffz-vip1x.png" + }, + "name": "", + "tooltip": "VIP" + }, + "flags": "BadgeChannelAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "VIP", + "trailingSpace": true, + "type": "VipBadgeElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "97bb0dfb-a35f-446d-8634-7522d8ef73ed" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "97bb0dfb-a35f-446d-8634-7522d8ef73ed", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "a", + "searchText": "nerixyz nerixyz: a ", + "serverReceivedTime": "2024-09-21T12:05:21Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "params": { + "ffzCustomVipBadge": true + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/emote-emoji.json b/tests/snapshots/MessageBuilder/IRC/emote-emoji.json new file mode 100644 index 000000000..70700cc33 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/emote-emoji.json @@ -0,0 +1,257 @@ +{ + "input": "@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emotes=25:0-4,8-12;first-msg=0;flags=;id=44f85d39-b5fb-475d-8555-f4244f2f7e82;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204423418;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa 😂 Kappa", + "output": [ + { + "badgeInfos": { + "subscriber": "80" + }, + "badges": [ + "broadcaster", + "subscriber", + "partner" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "pajlada", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "11:27" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "11:27:03", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/3" + }, + "name": "", + "tooltip": "Broadcaster" + }, + "flags": "BadgeChannelAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Broadcaster", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "images": { + "1x": "https://chatterino.com/tb-1", + "2x": "https://chatterino.com/tb-2", + "3x": "https://chatterino.com/tb-3" + }, + "name": "", + "tooltip": "Subscriber" + }, + "flags": "BadgeSubscription", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Subscriber (Tier 3, 80 months)", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "homePage": "https://blog.twitch.tv/2017/04/24/the-verified-badge-is-here-13381bc05735", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/d12a2e27-16f6-41d0-ab77-b780518f00a3/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/d12a2e27-16f6-41d0-ab77-b780518f00a3/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/d12a2e27-16f6-41d0-ab77-b780518f00a3/3" + }, + "name": "", + "tooltip": "Verified" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Verified", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffcc44ff", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "pajlada" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "pajlada:" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://pajbot.com/static/emoji-v2/img/twitter/64/1f602.png" + }, + "name": "😂", + "tooltip": ":joy:
Emoji" + }, + "flags": "EmojiImage|EmojiText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "😂" + ] + }, + "tooltip": ":joy:
Emoji", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "44f85d39-b5fb-475d-8555-f4244f2f7e82" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "44f85d39-b5fb-475d-8555-f4244f2f7e82", + "localizedName": "", + "loginName": "pajlada", + "messageText": "Kappa 😂 Kappa", + "searchText": "pajlada pajlada: Kappa 😂 Kappa ", + "serverReceivedTime": "2022-09-03T11:27:03Z", + "timeoutUser": "", + "usernameColor": "#ffcc44ff" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/emote.json b/tests/snapshots/MessageBuilder/IRC/emote.json new file mode 100644 index 000000000..5a9ec3fd2 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/emote.json @@ -0,0 +1,159 @@ +{ + "input": "@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=1902:0-4;first-msg=0;flags=;id=9b1c3cb9-7817-47ea-add1-f9d4a9b4f846;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201095690;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Keepo", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "no_audio" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "Mm2PL", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:31" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:31:35", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/3" + }, + "name": "", + "tooltip": "Watching without audio" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Watching without audio", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffdaa521", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "Mm2PL" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Mm2PL:" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/3.0" + }, + "name": "Keepo", + "tooltip": "Keepo
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Keepo" + ] + }, + "tooltip": "Keepo
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "9b1c3cb9-7817-47ea-add1-f9d4a9b4f846" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "9b1c3cb9-7817-47ea-add1-f9d4a9b4f846", + "localizedName": "", + "loginName": "mm2pl", + "messageText": "Keepo", + "searchText": "mm2pl mm2pl: Keepo ", + "serverReceivedTime": "2022-09-03T10:31:35Z", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/emotes.json b/tests/snapshots/MessageBuilder/IRC/emotes.json new file mode 100644 index 000000000..19d6bd7bf --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/emotes.json @@ -0,0 +1,227 @@ +{ + "input": "@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=25:0-4/1902:6-10/305954156:12-19;first-msg=0;flags=;id=7be87072-bf24-4fa3-b3df-0ea6fa5f1474;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201102276;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Kappa Keepo PogChamp", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "no_audio" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "Mm2PL", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:31" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:31:42", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/3" + }, + "name": "", + "tooltip": "Watching without audio" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Watching without audio", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffdaa521", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "Mm2PL" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Mm2PL:" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/3.0" + }, + "name": "Keepo", + "tooltip": "Keepo
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Keepo" + ] + }, + "tooltip": "Keepo
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/305954156/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/305954156/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/305954156/default/dark/3.0" + }, + "name": "PogChamp", + "tooltip": "PogChamp
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "PogChamp" + ] + }, + "tooltip": "PogChamp
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "7be87072-bf24-4fa3-b3df-0ea6fa5f1474" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "7be87072-bf24-4fa3-b3df-0ea6fa5f1474", + "localizedName": "", + "loginName": "mm2pl", + "messageText": "Kappa Keepo PogChamp", + "searchText": "mm2pl mm2pl: Kappa Keepo PogChamp ", + "serverReceivedTime": "2022-09-03T10:31:42Z", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/emotes2.json b/tests/snapshots/MessageBuilder/IRC/emotes2.json new file mode 100644 index 000000000..1fc89dbb6 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/emotes2.json @@ -0,0 +1,485 @@ +{ + "input": "@tmi-sent-ts=1726868573676;subscriber=1;id=23c8b81c-7c73-44e4-84f1-7d37e99714e4;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=25:10-14/305954156:50-57/1902:69-73 :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :BTTVEmote Kappa 7TVEmote 7TVEmote0w 7TVEmote0w 😂😂 PogChamp 7TVGlobal Keepo FFZEmote FFZGlobal", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "21:42" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "21:42:53", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/BTTVEmote", + "images": { + "1x": "https://chatterino.com/BTTVEmote.png" + }, + "name": "BTTVEmote", + "tooltip": "BTTVEmote Tooltip" + }, + "flags": "BttvEmoteImage|BttvEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "BTTVEmote" + ] + }, + "tooltip": "BTTVEmote Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emotes": [ + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/7TVEmote", + "id": "1", + "images": { + "1x": "https://chatterino.com/7TVEmote.png" + }, + "name": "7TVEmote", + "tooltip": "7TVEmote Tooltip" + }, + "flags": "SevenTVEmoteImage|SevenTVEmoteText" + }, + { + "emote": { + "author": "Chatterino", + "baseName": "ZeroWidth", + "homePage": "https://chatterino.com/7TVEmote0w", + "id": "2", + "images": { + "1x": "https://chatterino.com/7TVEmote0w.png" + }, + "name": "7TVEmote0w", + "tooltip": "7TVEmote0w Tooltip", + "zeroWidth": true + }, + "flags": "SevenTVEmoteImage|SevenTVEmoteText" + }, + { + "emote": { + "author": "Chatterino", + "baseName": "ZeroWidth", + "homePage": "https://chatterino.com/7TVEmote0w", + "id": "2", + "images": { + "1x": "https://chatterino.com/7TVEmote0w.png" + }, + "name": "7TVEmote0w", + "tooltip": "7TVEmote0w Tooltip", + "zeroWidth": true + }, + "flags": "SevenTVEmoteImage|SevenTVEmoteText" + } + ], + "flags": "SevenTVEmoteImage|SevenTVEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "7TVEmote", + "7TVEmote0w", + "7TVEmote0w" + ] + }, + "textElementColor": "Text", + "tooltip": "7TVEmote 7TVEmote0w 7TVEmote0w", + "tooltips": [ + ], + "trailingSpace": true, + "type": "LayeredEmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://pajbot.com/static/emoji-v2/img/twitter/64/1f602.png" + }, + "name": "😂", + "tooltip": ":joy:
Emoji" + }, + "flags": "EmojiImage|EmojiText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "😂" + ] + }, + "tooltip": ":joy:
Emoji", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://pajbot.com/static/emoji-v2/img/twitter/64/1f602.png" + }, + "name": "😂", + "tooltip": ":joy:
Emoji" + }, + "flags": "EmojiImage|EmojiText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "😂" + ] + }, + "tooltip": ":joy:
Emoji", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/305954156/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/305954156/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/305954156/default/dark/3.0" + }, + "name": "PogChamp", + "tooltip": "PogChamp
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "PogChamp" + ] + }, + "tooltip": "PogChamp
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/7TVGlobal", + "id": "G1", + "images": { + "1x": "https://chatterino.com/7TVGlobal.png" + }, + "name": "7TVGlobal", + "tooltip": "7TVGlobal Tooltip" + }, + "flags": "SevenTVEmoteImage|SevenTVEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "7TVGlobal" + ] + }, + "tooltip": "7TVGlobal Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/3.0" + }, + "name": "Keepo", + "tooltip": "Keepo
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Keepo" + ] + }, + "tooltip": "Keepo
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/FFZEmote", + "images": { + "1x": "https://chatterino.com/FFZEmote.png" + }, + "name": "FFZEmote", + "tooltip": "FFZEmote Tooltip" + }, + "flags": "FfzEmoteImage|FfzEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "FFZEmote" + ] + }, + "tooltip": "FFZEmote Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/FFZGlobal", + "images": { + "1x": "https://chatterino.com/FFZGlobal.png" + }, + "name": "FFZGlobal", + "tooltip": "FFZGlobal Tooltip" + }, + "flags": "FfzEmoteImage|FfzEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "FFZGlobal" + ] + }, + "tooltip": "FFZGlobal Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "23c8b81c-7c73-44e4-84f1-7d37e99714e4" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "23c8b81c-7c73-44e4-84f1-7d37e99714e4", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "BTTVEmote Kappa 7TVEmote 7TVEmote0w 7TVEmote0w 😂😂 PogChamp 7TVGlobal Keepo FFZEmote FFZGlobal", + "searchText": "nerixyz nerixyz: BTTVEmote Kappa 7TVEmote 7TVEmote0w 7TVEmote0w 😂😂 PogChamp 7TVGlobal Keepo FFZEmote FFZGlobal ", + "serverReceivedTime": "2024-09-20T21:42:53Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/emotes3.json b/tests/snapshots/MessageBuilder/IRC/emotes3.json new file mode 100644 index 000000000..9249074b9 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/emotes3.json @@ -0,0 +1,284 @@ +{ + "input": "@tmi-sent-ts=1726761013541;subscriber=1;id=aea562cc-1adf-47de-b656-0aaa245f1030;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=25:1-5/25:9-13 :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :😂Kappa😂 &Kappa a b", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "15:50" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "15:50:13", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "emote": { + "images": { + "1x": "https://pajbot.com/static/emoji-v2/img/twitter/64/1f602.png" + }, + "name": "😂", + "tooltip": ":joy:
Emoji" + }, + "flags": "EmojiImage|EmojiText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "😂" + ] + }, + "tooltip": ":joy:
Emoji", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": false, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://pajbot.com/static/emoji-v2/img/twitter/64/1f602.png" + }, + "name": "😂", + "tooltip": ":joy:
Emoji" + }, + "flags": "EmojiImage|EmojiText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "😂" + ] + }, + "tooltip": ":joy:
Emoji", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "&" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "b" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "aea562cc-1adf-47de-b656-0aaa245f1030" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "aea562cc-1adf-47de-b656-0aaa245f1030", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "😂Kappa😂 &Kappa a b", + "searchText": "nerixyz nerixyz: 😂Kappa😂 &Kappa a b ", + "serverReceivedTime": "2024-09-19T15:50:13Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/emotes4.json b/tests/snapshots/MessageBuilder/IRC/emotes4.json new file mode 100644 index 000000000..bf50ec23c --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/emotes4.json @@ -0,0 +1,169 @@ +{ + "input": "@tmi-sent-ts=1662201102276;emotes=84608:0-0 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "pajlada", + "count": 1, + "displayName": "", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:31" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:31:42", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ff999999", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "test:" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/84608/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/84608/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/84608/default/dark/3.0" + }, + "name": "f", + "tooltip": "f
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "f" + ] + }, + "tooltip": "f
Twitch Emote", + "trailingSpace": false, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "oo" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "bar" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "", + "localizedName": "", + "loginName": "test", + "messageText": "foo bar", + "searchText": "test test: foo bar ", + "serverReceivedTime": "2022-09-03T10:31:42Z", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/emotes5.json b/tests/snapshots/MessageBuilder/IRC/emotes5.json new file mode 100644 index 000000000..3b2e01b74 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/emotes5.json @@ -0,0 +1,169 @@ +{ + "input": "@tmi-sent-ts=1662201102276;emotes=84609:0-1 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "pajlada", + "count": 1, + "displayName": "", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:31" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:31:42", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ff999999", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "test:" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/84609/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/84609/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/84609/default/dark/3.0" + }, + "name": "fo", + "tooltip": "fo
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "fo" + ] + }, + "tooltip": "fo
Twitch Emote", + "trailingSpace": false, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "o" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "bar" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "", + "localizedName": "", + "loginName": "test", + "messageText": "foo bar", + "searchText": "test test: foo bar ", + "serverReceivedTime": "2022-09-03T10:31:42Z", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/first-msg.json b/tests/snapshots/MessageBuilder/IRC/first-msg.json new file mode 100644 index 000000000..ada382d14 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/first-msg.json @@ -0,0 +1,161 @@ +{ + "input": "@badge-info=subscriber/17;badges=subscriber/12,no_audio/1;color=#EBA2C0;display-name=jammehcow;emote-only=1;emotes=25:0-4;first-msg=1;flags=;id=9c2dd916-5a6d-4c1f-9fe7-a081b62a9c6b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662201093248;turbo=0;user-id=82674227;user-type= :jammehcow!jammehcow@jammehcow.tmi.twitch.tv PRIVMSG #pajlada :Kappa", + "output": [ + { + "badgeInfos": { + "subscriber": "17" + }, + "badges": [ + "subscriber", + "no_audio" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "jammehcow", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:31" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:31:33", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/3" + }, + "name": "", + "tooltip": "Watching without audio" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Watching without audio", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffeba2c0", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "jammehcow" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "jammehcow:" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "9c2dd916-5a6d-4c1f-9fe7-a081b62a9c6b" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|FirstMessage", + "id": "9c2dd916-5a6d-4c1f-9fe7-a081b62a9c6b", + "localizedName": "", + "loginName": "jammehcow", + "messageText": "Kappa", + "searchText": "jammehcow jammehcow: Kappa ", + "serverReceivedTime": "2022-09-03T10:31:33Z", + "timeoutUser": "", + "usernameColor": "#ffeba2c0" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/highlight1.json b/tests/snapshots/MessageBuilder/IRC/highlight1.json new file mode 100644 index 000000000..870c15a04 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/highlight1.json @@ -0,0 +1,152 @@ +{ + "input": "@tmi-sent-ts=1726865239492;subscriber=1;id=1bf2d49a-8b88-4116-81dd-5098f11726eb;room-id=11148817;user-id=129546453;badges=;color=#FF0000;flags=;user-type=;emotes= :ignoreduser!ignoreduser@ignoreduser.tmi.twitch.tv PRIVMSG #pajlada :my-highlight", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "pajlada", + "count": 1, + "displayName": "", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:47" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:47:19", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "ignoreduser:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "my-highlight" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "1bf2d49a-8b88-4116-81dd-5098f11726eb" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "1bf2d49a-8b88-4116-81dd-5098f11726eb", + "localizedName": "", + "loginName": "ignoreduser", + "messageText": "my-highlight", + "searchText": "ignoreduser ignoreduser: my-highlight ", + "serverReceivedTime": "2024-09-20T20:47:19Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "settings": { + "highlighting": { + "blacklist": [ + { + "pattern": "ignoreduser", + "regex": false + } + ], + "highlights": [ + { + "alert": false, + "case": false, + "color": "#7f7f3f49", + "pattern": "my-highlight", + "regex": false, + "showInMentions": true, + "sound": true, + "soundUrl": "" + }, + { + "alert": false, + "case": false, + "color": "#48ae812f", + "pattern": "no-mention-highlight", + "regex": false, + "showInMentions": false, + "sound": true, + "soundUrl": "" + } + ] + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/highlight2.json b/tests/snapshots/MessageBuilder/IRC/highlight2.json new file mode 100644 index 000000000..aff7ba1d4 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/highlight2.json @@ -0,0 +1,179 @@ +{ + "input": "@tmi-sent-ts=1726866916736;subscriber=1;id=7b5d2152-8eec-41ce-83eb-999ce5c21d28;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :something my-highlight *", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "21:15" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "21:15:16", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "something" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "my-highlight" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "*" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "7b5d2152-8eec-41ce-83eb-999ce5c21d28" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Highlighted|Collapsed|ShowInMentions", + "highlightColor": "#7f7f3f49", + "id": "7b5d2152-8eec-41ce-83eb-999ce5c21d28", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "something my-highlight *", + "searchText": "nerixyz nerixyz: something my-highlight * ", + "serverReceivedTime": "2024-09-20T21:15:16Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "settings": { + "highlighting": { + "highlights": [ + { + "alert": false, + "case": false, + "color": "#7f7f3f49", + "pattern": "my-highlight", + "regex": false, + "showInMentions": true, + "sound": true, + "soundUrl": "" + }, + { + "alert": false, + "case": false, + "color": "#48ae812f", + "pattern": "no-mention-highlight", + "regex": false, + "showInMentions": false, + "sound": true, + "soundUrl": "" + } + ] + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/highlight3.json b/tests/snapshots/MessageBuilder/IRC/highlight3.json new file mode 100644 index 000000000..f60774fc9 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/highlight3.json @@ -0,0 +1,164 @@ +{ + "input": "@tmi-sent-ts=1726866927483;subscriber=1;id=f72f5a7a-522d-407c-ac16-a0b413632fa9;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :foo no-mention-highlight", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "21:15" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "21:15:27", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "no-mention-highlight" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "f72f5a7a-522d-407c-ac16-a0b413632fa9" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Highlighted|Collapsed", + "highlightColor": "#48ae812f", + "id": "f72f5a7a-522d-407c-ac16-a0b413632fa9", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "foo no-mention-highlight", + "searchText": "nerixyz nerixyz: foo no-mention-highlight ", + "serverReceivedTime": "2024-09-20T21:15:27Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "settings": { + "highlighting": { + "highlights": [ + { + "alert": false, + "case": false, + "color": "#7f7f3f49", + "pattern": "my-highlight", + "regex": false, + "showInMentions": true, + "sound": true, + "soundUrl": "" + }, + { + "alert": false, + "case": false, + "color": "#48ae812f", + "pattern": "no-mention-highlight", + "regex": false, + "showInMentions": false, + "sound": true, + "soundUrl": "" + } + ] + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/hype-chat-invalid.json b/tests/snapshots/MessageBuilder/IRC/hype-chat-invalid.json new file mode 100644 index 000000000..414077792 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/hype-chat-invalid.json @@ -0,0 +1,162 @@ +{ + "input": "@badge-info=subscriber/3;badges=subscriber/0,bits-charity/1;color=#0000FF;display-name=SnoopyTheBot;emotes=;first-msg=0;flags=;id=8779a9e5-cf1b-47b3-b9fe-67a5b1b605f6;mod=0;pinned-chat-paid-amount=5-00;pinned-chat-paid-canonical-amount=5;pinned-chat-paid-currency=USD;pinned-chat-paid-exponent=2;returning-chatter=0;room-id=36340781;subscriber=1;tmi-sent-ts=1664505974154;turbo=0;user-id=136881249;user-type= :snoopythebot!snoopythebot@snoopythebot.tmi.twitch.tv PRIVMSG #pajlada :-$5", + "output": [ + { + "badgeInfos": { + "subscriber": "3" + }, + "badges": [ + "subscriber", + "bits-charity" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "SnoopyTheBot", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2:46" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "02:46:14", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/5d9f2208-5dd8-11e7-8513-2ff4adfae661/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/5d9f2208-5dd8-11e7-8513-2ff4adfae661/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/5d9f2208-5dd8-11e7-8513-2ff4adfae661/3" + }, + "name": "", + "tooltip": "Subscriber" + }, + "flags": "BadgeSubscription", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Subscriber (3 months)", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "homePage": "https://link.twitch.tv/blizzardofbits", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/a539dc18-ae19-49b0-98c4-8391a594332b/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/a539dc18-ae19-49b0-98c4-8391a594332b/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/a539dc18-ae19-49b0-98c4-8391a594332b/3" + }, + "name": "", + "tooltip": "Direct Relief - Charity 2018" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Direct Relief - Charity 2018", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff0000ff", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "SnoopyTheBot" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "SnoopyTheBot:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "-$5" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "8779a9e5-cf1b-47b3-b9fe-67a5b1b605f6" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|ElevatedMessage", + "id": "8779a9e5-cf1b-47b3-b9fe-67a5b1b605f6", + "localizedName": "", + "loginName": "snoopythebot", + "messageText": "-$5", + "searchText": "snoopythebot snoopythebot: -$5 ", + "serverReceivedTime": "2022-09-30T02:46:14Z", + "timeoutUser": "", + "usernameColor": "#ff0000ff" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/hype-chat0.json b/tests/snapshots/MessageBuilder/IRC/hype-chat0.json new file mode 100644 index 000000000..f22bd0e0f --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/hype-chat0.json @@ -0,0 +1,254 @@ +{ + "input": "@badge-info=subscriber/3;badges=subscriber/0,bits-charity/1;color=#0000FF;display-name=SnoopyTheBot;emotes=;first-msg=0;flags=;id=8779a9e5-cf1b-47b3-b9fe-67a5b1b605f6;mod=0;pinned-chat-paid-amount=500;pinned-chat-paid-canonical-amount=5;pinned-chat-paid-currency=USD;pinned-chat-paid-exponent=2;returning-chatter=0;room-id=36340781;subscriber=1;tmi-sent-ts=1664505974154;turbo=0;user-id=136881249;user-type= :snoopythebot!snoopythebot@snoopythebot.tmi.twitch.tv PRIVMSG #pajlada :-$5", + "output": [ + { + "badgeInfos": { + "subscriber": "3" + }, + "badges": [ + "subscriber", + "bits-charity" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "SnoopyTheBot", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2:46" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "02:46:14", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/5d9f2208-5dd8-11e7-8513-2ff4adfae661/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/5d9f2208-5dd8-11e7-8513-2ff4adfae661/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/5d9f2208-5dd8-11e7-8513-2ff4adfae661/3" + }, + "name": "", + "tooltip": "Subscriber" + }, + "flags": "BadgeSubscription", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Subscriber (3 months)", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "homePage": "https://link.twitch.tv/blizzardofbits", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/a539dc18-ae19-49b0-98c4-8391a594332b/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/a539dc18-ae19-49b0-98c4-8391a594332b/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/a539dc18-ae19-49b0-98c4-8391a594332b/3" + }, + "name": "", + "tooltip": "Direct Relief - Charity 2018" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Direct Relief - Charity 2018", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff0000ff", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "SnoopyTheBot" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "SnoopyTheBot:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "-$5" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "8779a9e5-cf1b-47b3-b9fe-67a5b1b605f6" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|ElevatedMessage", + "id": "8779a9e5-cf1b-47b3-b9fe-67a5b1b605f6", + "localizedName": "", + "loginName": "snoopythebot", + "messageText": "-$5", + "searchText": "snoopythebot snoopythebot: -$5 ", + "serverReceivedTime": "2022-09-30T02:46:14Z", + "timeoutUser": "", + "usernameColor": "#ff0000ff" + }, + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2:46" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "02:46:14", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Hype" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Chat" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "USD5.00" + ] + } + ], + "flags": "System|DoNotTriggerNotification|ElevatedMessage", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "Hype Chat USD5.00", + "searchText": "Hype Chat USD5.00", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/hype-chat1.json b/tests/snapshots/MessageBuilder/IRC/hype-chat1.json new file mode 100644 index 000000000..2d709c606 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/hype-chat1.json @@ -0,0 +1,406 @@ +{ + "input": "@pinned-chat-paid-level=ONE;mod=0;flags=;pinned-chat-paid-amount=1400;pinned-chat-paid-exponent=2;tmi-sent-ts=1687970631828;subscriber=1;user-type=;color=#9DA364;emotes=;badges=predictions/blue-1,subscriber/60,twitchconAmsterdam2020/1;pinned-chat-paid-canonical-amount=1400;turbo=0;user-id=26753388;id=e6681ba0-cdc6-4482-93a3-515b74361e8b;room-id=36340781;first-msg=0;returning-chatter=0;pinned-chat-paid-currency=NOK;pinned-chat-paid-is-system-message=0;badge-info=predictions/Day\\s53/53\\sforsenSmug,subscriber/67;display-name=matrHS :matrhs!matrhs@matrhs.tmi.twitch.tv PRIVMSG #pajlada :Title: Beating the record. but who is recordingLOL", + "output": [ + { + "badgeInfos": { + "predictions": "Day\\s53/53\\sforsenSmug", + "subscriber": "67" + }, + "badges": [ + "predictions", + "subscriber", + "twitchconAmsterdam2020" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "matrHS", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "16:43" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "16:43:51", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/e33d8b46-f63b-4e67-996d-4a7dcec0ad33/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/e33d8b46-f63b-4e67-996d-4a7dcec0ad33/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/e33d8b46-f63b-4e67-996d-4a7dcec0ad33/3" + }, + "name": "", + "tooltip": "Predicted Blue (1)" + }, + "flags": "BadgePredictions", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Predicted Day 53/53 forsenSmug", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "homePage": "https://www.twitchcon.com/amsterdam/?utm_source=twitch-chat&utm_medium=badge&utm_campaign=tcamsterdam20", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/ed917c9a-1a45-4340-9c64-ca8be4348c51/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/ed917c9a-1a45-4340-9c64-ca8be4348c51/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/ed917c9a-1a45-4340-9c64-ca8be4348c51/3" + }, + "name": "", + "tooltip": "TwitchCon 2020 - Amsterdam" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "TwitchCon 2020 - Amsterdam", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff9da364", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "matrHS" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "matrHS:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Title:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Beating" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "the" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "record." + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "but" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "who" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "is" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "recordingLOL" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "e6681ba0-cdc6-4482-93a3-515b74361e8b" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|ElevatedMessage", + "id": "e6681ba0-cdc6-4482-93a3-515b74361e8b", + "localizedName": "", + "loginName": "matrhs", + "messageText": "Title: Beating the record. but who is recordingLOL", + "searchText": "matrhs matrhs: Title: Beating the record. but who is recordingLOL ", + "serverReceivedTime": "2023-06-28T16:43:51Z", + "timeoutUser": "", + "usernameColor": "#ff9da364" + }, + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "16:43" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "16:43:51", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Level" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Hype" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Chat" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "(30s)" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "NOK14.00" + ] + } + ], + "flags": "System|DoNotTriggerNotification|ElevatedMessage", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "Level 1 Hype Chat (30s) NOK14.00", + "searchText": "Level 1 Hype Chat (30s) NOK14.00", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/hype-chat2.json b/tests/snapshots/MessageBuilder/IRC/hype-chat2.json new file mode 100644 index 000000000..20d8b872d --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/hype-chat2.json @@ -0,0 +1,404 @@ +{ + "input": "@room-id=36340781;tmi-sent-ts=1687970634371;flags=;id=39a80a3d-c16e-420f-9bbb-faba4976a3bb;badges=subscriber/6,premium/1;emotes=;display-name=rickharrisoncoc;pinned-chat-paid-level=TWO;turbo=0;pinned-chat-paid-amount=500;pinned-chat-paid-is-system-message=0;color=#FF69B4;subscriber=1;user-type=;first-msg=0;pinned-chat-paid-currency=USD;pinned-chat-paid-canonical-amount=500;user-id=518404689;badge-info=subscriber/10;pinned-chat-paid-exponent=2;returning-chatter=0;mod=0 :rickharrisoncoc!rickharrisoncoc@rickharrisoncoc.tmi.twitch.tv PRIVMSG #pajlada :forsen please read my super chat. Please.", + "output": [ + { + "badgeInfos": { + "subscriber": "10" + }, + "badges": [ + "subscriber", + "premium" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "rickharrisoncoc", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "16:43" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "16:43:54", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/ed51a614-2c44-4a60-80b6-62908436b43a/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/ed51a614-2c44-4a60-80b6-62908436b43a/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/ed51a614-2c44-4a60-80b6-62908436b43a/3" + }, + "name": "", + "tooltip": "6-Month Subscriber" + }, + "flags": "BadgeSubscription", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "6-Month Subscriber (10 months)", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "homePage": "https://gaming.amazon.com", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/bbbe0db0-a598-423e-86d0-f9fb98ca1933/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/bbbe0db0-a598-423e-86d0-f9fb98ca1933/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/bbbe0db0-a598-423e-86d0-f9fb98ca1933/3" + }, + "name": "", + "tooltip": "Prime Gaming" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Prime Gaming", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffff69b4", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "rickharrisoncoc" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "rickharrisoncoc:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "forsen" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "please" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "read" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "my" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "super" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "chat." + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Please." + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "39a80a3d-c16e-420f-9bbb-faba4976a3bb" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|ElevatedMessage", + "id": "39a80a3d-c16e-420f-9bbb-faba4976a3bb", + "localizedName": "", + "loginName": "rickharrisoncoc", + "messageText": "forsen please read my super chat. Please.", + "searchText": "rickharrisoncoc rickharrisoncoc: forsen please read my super chat. Please. ", + "serverReceivedTime": "2023-06-28T16:43:54Z", + "timeoutUser": "", + "usernameColor": "#ffff69b4" + }, + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "16:43" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "16:43:54", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Level" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Hype" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Chat" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "(2m" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "30s)" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "USD5.00" + ] + } + ], + "flags": "System|DoNotTriggerNotification|ElevatedMessage", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "Level 2 Hype Chat (2m 30s) USD5.00", + "searchText": "Level 2 Hype Chat (2m 30s) USD5.00", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/ignore-block1.json b/tests/snapshots/MessageBuilder/IRC/ignore-block1.json new file mode 100644 index 000000000..94467cb05 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/ignore-block1.json @@ -0,0 +1,163 @@ +{ + "input": "@tmi-sent-ts=1726692539140;subscriber=1;id=c34a372b-d59e-4a36-9ad8-d964098526e4;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :block!", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:48" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:48:59", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "block!" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "c34a372b-d59e-4a36-9ad8-d964098526e4" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "c34a372b-d59e-4a36-9ad8-d964098526e4", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "block!", + "searchText": "nerixyz nerixyz: block! ", + "serverReceivedTime": "2024-09-18T20:48:59Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "settings": { + "ignore": { + "phrases": [ + { + "caseSensitive": false, + "isBlock": false, + "pattern": "ignore", + "regex": false, + "replaceWith": "replace" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "BLOCK", + "regex": false, + "replaceWith": "?" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "block!{2,}", + "regex": true, + "replaceWith": "?" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "", + "regex": false, + "replaceWith": "empty" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "(", + "regex": true, + "replaceWith": "invalid" + } + ] + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/ignore-block2.json b/tests/snapshots/MessageBuilder/IRC/ignore-block2.json new file mode 100644 index 000000000..119db3bbf --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/ignore-block2.json @@ -0,0 +1,32 @@ +{ + "input": "@tmi-sent-ts=1726692555236;subscriber=1;id=38a32590-81c6-460b-a49d-fcf2bec0f788;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :somethingblock!!something", + "output": [ + ], + "settings": { + "ignore": { + "phrases": [ + { + "caseSensitive": false, + "isBlock": false, + "pattern": "ignore", + "regex": false, + "replaceWith": "replace" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "block!{2,}", + "regex": true, + "replaceWith": "?" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "", + "regex": false, + "replaceWith": "empty" + } + ] + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/ignore-infinite.json b/tests/snapshots/MessageBuilder/IRC/ignore-infinite.json new file mode 100644 index 000000000..28030cafa --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/ignore-infinite.json @@ -0,0 +1,225 @@ +{ + "input": "@tmi-sent-ts=1726865562691;subscriber=1;id=08e1573a-c09a-4052-8cf6-dd9fc5edf35b;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :this is an infinite-loop", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:52" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:52:42", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Too" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "many" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "replacements" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "-" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "check" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "your" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "ignores!" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "08e1573a-c09a-4052-8cf6-dd9fc5edf35b" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "08e1573a-c09a-4052-8cf6-dd9fc5edf35b", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "Too many replacements - check your ignores!", + "searchText": "nerixyz nerixyz: Too many replacements - check your ignores! ", + "serverReceivedTime": "2024-09-20T20:52:42Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "settings": { + "ignore": { + "phrases": [ + { + "caseSensitive": true, + "isBlock": false, + "pattern": "(?<=infinite-loop)$", + "regex": true, + "replaceWith": "infinite-loop" + } + ] + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/ignore-replace.json b/tests/snapshots/MessageBuilder/IRC/ignore-replace.json new file mode 100644 index 000000000..23f92df75 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/ignore-replace.json @@ -0,0 +1,540 @@ +{ + "input": "@tmi-sent-ts=1726925714864;subscriber=1;id=2199102c-31ae-49b1-9d2c-a33bb3a02021;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=305954156:0-7/25:16-20,92-96/1902:31-35 :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :PogChamp ignore Kappa &fooo123 Keepo &boo1 &baa1 &bi1 &biii1 &biiiiiiiiii420 &foo123&fo2 &[ Kappa ]& summon-emote", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "13:35" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "13:35:14", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/305954156/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/305954156/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/305954156/default/dark/3.0" + }, + "name": "PogChamp", + "tooltip": "PogChamp
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "PogChamp" + ] + }, + "tooltip": "PogChamp
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "replace" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "&baz1[ooo+123]" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/3.0" + }, + "name": "Keepo", + "tooltip": "Keepo
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Keepo" + ] + }, + "tooltip": "Keepo
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "&baz2[1+\\2]" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "&baz3[1+\\42]" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "&bi1" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "&biii1" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "&baz4[i+420+i]" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "&baz1[oo+123]&baz1[o+2]" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "{" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "}" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "woah->" + ] + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/MyCoolTwitchEmote", + "id": "5678", + "images": { + "1x": "https://chatterino.com/MyCoolTwitchEmote.png" + }, + "name": "MyCoolTwitchEmote", + "tooltip": "MyCoolTwitchEmote Tooltip" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "MyCoolTwitchEmote" + ] + }, + "tooltip": "MyCoolTwitchEmote Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "2199102c-31ae-49b1-9d2c-a33bb3a02021" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "2199102c-31ae-49b1-9d2c-a33bb3a02021", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "PogChamp replace Kappa &baz1[ooo+123] Keepo &baz2[1+\\2] &baz3[1+\\42] &bi1 &biii1 &baz4[i+420+i] &baz1[oo+123]&baz1[o+2] { Kappa } woah-> MyCoolTwitchEmote", + "searchText": "nerixyz nerixyz: PogChamp replace Kappa &baz1[ooo+123] Keepo &baz2[1+\\2] &baz3[1+\\42] &bi1 &biii1 &baz4[i+420+i] &baz1[oo+123]&baz1[o+2] { Kappa } woah-> MyCoolTwitchEmote ", + "serverReceivedTime": "2024-09-21T13:35:14Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "settings": { + "ignore": { + "phrases": [ + { + "caseSensitive": false, + "isBlock": false, + "pattern": "ignore", + "regex": false, + "replaceWith": "replace" + }, + { + "caseSensitive": true, + "isBlock": false, + "pattern": "CaseSensitive", + "regex": false, + "replaceWith": "casesensitivE" + }, + { + "caseSensitive": true, + "isBlock": false, + "pattern": "summon-emote", + "regex": false, + "replaceWith": "woah-> MyCoolTwitchEmote" + }, + { + "caseSensitive": false, + "isBlock": false, + "pattern": "&f(o+)(\\d+)", + "regex": true, + "replaceWith": "&baz1[\\1+\\2]" + }, + { + "caseSensitive": false, + "isBlock": false, + "pattern": "&b(?:o+)(\\d+)", + "regex": true, + "replaceWith": "&baz2[\\1+\\2]" + }, + { + "caseSensitive": false, + "isBlock": false, + "pattern": "&b(?:a+)(\\d+)", + "regex": true, + "replaceWith": "&baz3[\\1+\\42]" + }, + { + "caseSensitive": false, + "isBlock": false, + "pattern": "&b(i)(i)(i)(i)(i)(i)(i)(i)(i)(i)(\\d+)", + "regex": true, + "replaceWith": "&baz4[\\10+\\11+\\1]" + }, + { + "caseSensitive": false, + "isBlock": false, + "pattern": "&\\[ (\\w+) \\]&", + "regex": true, + "replaceWith": "{ \\1 }" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "BLOCK", + "regex": false, + "replaceWith": "?" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "block!{2,}", + "regex": true, + "replaceWith": "?" + }, + { + "caseSensitive": true, + "isBlock": false, + "pattern": "", + "regex": false, + "replaceWith": "empty" + }, + { + "caseSensitive": true, + "isBlock": false, + "pattern": "(", + "regex": true, + "replaceWith": "invalid" + }, + { + "caseSensitive": true, + "isBlock": false, + "pattern": "(?<=infinite-loop)$", + "regex": true, + "replaceWith": "infinite-loop" + } + ] + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/justinfan.json b/tests/snapshots/MessageBuilder/IRC/justinfan.json new file mode 100644 index 000000000..0ed4190ad --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/justinfan.json @@ -0,0 +1,122 @@ +{ + "input": "@tmi-sent-ts=1726918643421;subscriber=1;id=e5a690e7-74c8-41cb-977f-55b903f6be23;room-id=11148817;user-id=64537;display-name=justinfan64537;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :justinfan64537!justinfan64537@justinfan64537.tmi.twitch.tv PRIVMSG #pajlada :a", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "justinfan64537", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "11:37" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "11:37:23", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "justinfan64537" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "justinfan64537:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "e5a690e7-74c8-41cb-977f-55b903f6be23" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "e5a690e7-74c8-41cb-977f-55b903f6be23", + "localizedName": "", + "loginName": "justinfan64537", + "messageText": "a", + "searchText": "justinfan64537 justinfan64537: a ", + "serverReceivedTime": "2024-09-21T11:37:23Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/links.json b/tests/snapshots/MessageBuilder/IRC/links.json new file mode 100644 index 000000000..7a3325d2c --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/links.json @@ -0,0 +1,239 @@ +{ + "input": "@tmi-sent-ts=1726678491958;subscriber=1;id=84b6f44a-c8eb-4abe-8a78-cf736642f695;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :https://chatterino.com (chatterino.com chatterino.com) (chatterino.com)", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "16:54" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "16:54:51", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Link", + "flags": "Text", + "link": "https://chatterino.com", + "lowercase": [ + "https://chatterino.com" + ], + "original": [ + "https://chatterino.com" + ], + "style": "ChatMedium", + "tooltip": "https://chatterino.com", + "trailingSpace": true, + "type": "LinkElement", + "words": [ + "" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": false, + "type": "TextElement", + "words": [ + "(" + ] + }, + { + "color": "Link", + "flags": "Text", + "link": "http://chatterino.com", + "lowercase": [ + "chatterino.com" + ], + "original": [ + "chatterino.com" + ], + "style": "ChatMedium", + "tooltip": "chatterino.com", + "trailingSpace": true, + "type": "LinkElement", + "words": [ + "" + ] + }, + { + "color": "Link", + "flags": "Text", + "link": "http://chatterino.com", + "lowercase": [ + "chatterino.com" + ], + "original": [ + "chatterino.com" + ], + "style": "ChatMedium", + "tooltip": "chatterino.com", + "trailingSpace": false, + "type": "LinkElement", + "words": [ + "" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + ")" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": false, + "type": "TextElement", + "words": [ + "(" + ] + }, + { + "color": "Link", + "flags": "Text", + "link": "http://chatterino.com", + "lowercase": [ + "chatterino.com" + ], + "original": [ + "chatterino.com" + ], + "style": "ChatMedium", + "tooltip": "chatterino.com", + "trailingSpace": false, + "type": "LinkElement", + "words": [ + "" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + ")" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "84b6f44a-c8eb-4abe-8a78-cf736642f695" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "84b6f44a-c8eb-4abe-8a78-cf736642f695", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "https://chatterino.com (chatterino.com chatterino.com) (chatterino.com)", + "searchText": "nerixyz nerixyz: https://chatterino.com (chatterino.com chatterino.com) (chatterino.com) ", + "serverReceivedTime": "2024-09-18T16:54:51Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/mentions.json b/tests/snapshots/MessageBuilder/IRC/mentions.json new file mode 100644 index 000000000..4c52bc389 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/mentions.json @@ -0,0 +1,173 @@ +{ + "input": "@tmi-sent-ts=1726678643417;subscriber=1;id=49f33e31-101f-4b03-bfc4-4b8252d0c09c;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@~ @TwitchDev @UserColor!", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "16:57" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "16:57:23", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "@~" + ] + }, + { + "color": "Text", + "fallbackColor": "Text", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "Text", + "userLoginName": "TwitchDev", + "words": [ + "@TwitchDev" + ] + }, + { + "color": "Text", + "fallbackColor": "Text", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": false, + "type": "MentionElement", + "userColor": "#ff010203", + "userLoginName": "UserColor", + "words": [ + "@UserColor" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "!" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "49f33e31-101f-4b03-bfc4-4b8252d0c09c" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "49f33e31-101f-4b03-bfc4-4b8252d0c09c", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "@~ @TwitchDev @UserColor!", + "searchText": "nerixyz nerixyz: @~ @TwitchDev @UserColor! ", + "serverReceivedTime": "2024-09-18T16:57:23Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/mod.json b/tests/snapshots/MessageBuilder/IRC/mod.json new file mode 100644 index 000000000..f0c632c67 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/mod.json @@ -0,0 +1,215 @@ +{ + "input": "@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=41:6-13,16-23;flags=;id=97c28382-e8d2-45a0-bb5d-2305fc4ef139;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922036771;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :-tags Kreygasm, Kreygasm", + "output": [ + { + "badgeInfos": { + "subscriber": "34" + }, + "badges": [ + "moderator", + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "testaccount_420", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:47" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:47:16", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/3267646d-33f0-4b17-b3df-f923a41db1d0/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/3267646d-33f0-4b17-b3df-f923a41db1d0/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/3267646d-33f0-4b17-b3df-f923a41db1d0/3" + }, + "name": "", + "tooltip": "Moderator" + }, + "flags": "BadgeChannelAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Moderator", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "testaccount_420" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "testaccount_420(테스트계정420):" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "-tags" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/41/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/41/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/41/default/dark/3.0" + }, + "name": "Kreygasm", + "tooltip": "Kreygasm
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kreygasm" + ] + }, + "tooltip": "Kreygasm
Twitch Emote", + "trailingSpace": false, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "," + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/41/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/41/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/41/default/dark/3.0" + }, + "name": "Kreygasm", + "tooltip": "Kreygasm
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kreygasm" + ] + }, + "tooltip": "Kreygasm
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "97c28382-e8d2-45a0-bb5d-2305fc4ef139" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "97c28382-e8d2-45a0-bb5d-2305fc4ef139", + "localizedName": "테스트계정420", + "loginName": "testaccount_420", + "messageText": "-tags Kreygasm, Kreygasm", + "searchText": "testaccount_420(테스트계정420) 테스트계정420 testaccount_420: -tags Kreygasm, Kreygasm ", + "serverReceivedTime": "2020-05-31T10:47:16Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/nickname.json b/tests/snapshots/MessageBuilder/IRC/nickname.json new file mode 100644 index 000000000..8535e03dc --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/nickname.json @@ -0,0 +1,132 @@ +{ + "input": "@tmi-sent-ts=1726915984645;subscriber=1;id=a964d705-0b72-4867-aab7-bc9a14945742;room-id=11148817;user-id=12345678;display-name=NickName;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nickname!nickname@nickname.tmi.twitch.tv PRIVMSG #pajlada :nickname", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "NickName", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:53" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:53:04", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "NickName" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "replacement:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nickname" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "a964d705-0b72-4867-aab7-bc9a14945742" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "a964d705-0b72-4867-aab7-bc9a14945742", + "localizedName": "", + "loginName": "nickname", + "messageText": "nickname", + "searchText": "replacement nickname: nickname ", + "serverReceivedTime": "2024-09-21T10:53:04Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "settings": { + "nicknames": [ + { + "isCaseSensitive": false, + "isRegex": false, + "name": "nickname", + "replace": "replacement" + } + ] + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/no-nick.json b/tests/snapshots/MessageBuilder/IRC/no-nick.json new file mode 100644 index 000000000..db59fbd29 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/no-nick.json @@ -0,0 +1,122 @@ +{ + "input": "@tmi-sent-ts=1726865239492;subscriber=1;id=1bf2d49a-8b88-4116-81dd-5098f11726eb;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :no", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:47" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:47:19", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "(nerixyz):" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "no" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "1bf2d49a-8b88-4116-81dd-5098f11726eb" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "1bf2d49a-8b88-4116-81dd-5098f11726eb", + "localizedName": "nerixyz", + "loginName": "", + "messageText": "no", + "searchText": "(nerixyz) nerixyz : no ", + "serverReceivedTime": "2024-09-20T20:47:19Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/no-tags.json b/tests/snapshots/MessageBuilder/IRC/no-tags.json new file mode 100644 index 000000000..103148bc5 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/no-tags.json @@ -0,0 +1,139 @@ +{ + "input": "@tmi-sent-ts=1726764056444 :jammehcow!jammehcow@jammehcow.tmi.twitch.tv PRIVMSG #pajlada :Kappa", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "pajlada", + "count": 1, + "displayName": "", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "16:40" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "16:40:56", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ff999999", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "jammehcow:" + ] + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/Kappa", + "images": { + "1x": "https://chatterino.com/Kappa.png" + }, + "name": "Kappa", + "tooltip": "Kappa Tooltip" + }, + "flags": "BttvEmoteImage|BttvEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "", + "localizedName": "", + "loginName": "jammehcow", + "messageText": "Kappa", + "searchText": "jammehcow jammehcow: Kappa ", + "serverReceivedTime": "2024-09-19T16:40:56Z", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/redeemed-highlight.json b/tests/snapshots/MessageBuilder/IRC/redeemed-highlight.json new file mode 100644 index 000000000..e81342e9e --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/redeemed-highlight.json @@ -0,0 +1,190 @@ +{ + "input": "@tmi-sent-ts=1726662616942;subscriber=1;id=c5a927a2-1f68-41f7-9137-809a37e58e61;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=1:21-22;msg-id=highlighted-message :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :BTTVGlobal highlight :)", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "12:30" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "12:30:16", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/BTTVGlobal", + "images": { + "1x": "https://chatterino.com/BTTVGlobal.png" + }, + "name": "BTTVGlobal", + "tooltip": "BTTVGlobal Tooltip" + }, + "flags": "BttvEmoteImage|BttvEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "BTTVGlobal" + ] + }, + "tooltip": "BTTVGlobal Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "highlight" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/1/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/1/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/1/default/dark/3.0" + }, + "name": ":)", + "tooltip": ":)
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + ":)" + ] + }, + "tooltip": ":)
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "c5a927a2-1f68-41f7-9137-809a37e58e61" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|RedeemedHighlight", + "id": "c5a927a2-1f68-41f7-9137-809a37e58e61", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "BTTVGlobal highlight :)", + "searchText": "nerixyz nerixyz: BTTVGlobal highlight :) ", + "serverReceivedTime": "2024-09-18T12:30:16Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/reply-action.json b/tests/snapshots/MessageBuilder/IRC/reply-action.json new file mode 100644 index 000000000..6f75671c6 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reply-action.json @@ -0,0 +1,191 @@ +{ + "input": "@tmi-sent-ts=1726691189926;subscriber=1;id=9d74021f-375a-44f1-80e4-0dd58e64396a;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;reply-thread-parent-display-name=nerixyz;reply-parent-display-name=nerixyz;reply-thread-parent-msg-id=c6ff10fb-7eed-4326-b7ff-bf2b77b9b021;reply-thread-parent-user-login=nerixyz;reply-parent-user-login=nerixyz;reply-thread-parent-user-id=129546453;reply-parent-msg-body=a;reply-parent-msg-id=c6ff10fb-7eed-4326-b7ff-bf2b77b9b021;reply-parent-user-id=129546453 :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@nerixyz b", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ReplyCurveElement" + }, + { + "color": "System", + "flags": "RepliedMessage", + "link": { + "type": "ViewThread", + "value": "c6ff10fb-7eed-4326-b7ff-bf2b77b9b021" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Replying", + "to" + ] + }, + { + "color": "#ffff0000", + "flags": "RepliedMessage", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "@nerixyz" + ] + }, + { + "color": "#ffff0000", + "flags": "Text|RepliedMessage", + "link": { + "type": "ViewThread", + "value": "c6ff10fb-7eed-4326-b7ff-bf2b77b9b021" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "a" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:26" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:26:29", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "b" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ViewThread", + "value": "c6ff10fb-7eed-4326-b7ff-bf2b77b9b021" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|ReplyMessage", + "id": "9d74021f-375a-44f1-80e4-0dd58e64396a", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "b", + "replyParent": "c6ff10fb-7eed-4326-b7ff-bf2b77b9b021", + "replyThread": { + "replies": [ + "9d74021f-375a-44f1-80e4-0dd58e64396a" + ], + "rootId": "c6ff10fb-7eed-4326-b7ff-bf2b77b9b021", + "subscription": "None" + }, + "searchText": "nerixyz nerixyz: b ", + "serverReceivedTime": "2024-09-18T20:26:29Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "params": { + "prevMessages": [ + "@tmi-sent-ts=1726691182314;subscriber=1;id=c6ff10fb-7eed-4326-b7ff-bf2b77b9b021;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :\u0001ACTION a\u0001" + ] + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/reply-block.json b/tests/snapshots/MessageBuilder/IRC/reply-block.json new file mode 100644 index 000000000..600828ae5 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reply-block.json @@ -0,0 +1,217 @@ +{ + "input": "@tmi-sent-ts=1726692474917;subscriber=1;id=289ffa22-d29a-4300-88be-1734b0a33b2a;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;reply-parent-msg-body=BLOCK;reply-thread-parent-user-login=nerixyz;reply-thread-parent-user-id=129546453;reply-parent-user-login=nerixyz;reply-thread-parent-display-name=nerixyz;reply-parent-msg-id=4d2af478-c471-4b3a-8c6f-568a54d2fe7a;reply-thread-parent-msg-id=4d2af478-c471-4b3a-8c6f-568a54d2fe7a;reply-parent-display-name=nerixyz;reply-parent-user-id=129546453 :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@nerixyz reply", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ReplyCurveElement" + }, + { + "color": "System", + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Replying", + "to" + ] + }, + { + "color": "Text", + "flags": "RepliedMessage", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "@nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text|RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "BLOCK" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:47" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:47:54", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "reply" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "289ffa22-d29a-4300-88be-1734b0a33b2a" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "289ffa22-d29a-4300-88be-1734b0a33b2a", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "reply", + "searchText": "nerixyz nerixyz: reply ", + "serverReceivedTime": "2024-09-18T20:47:54Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "params": { + "prevMessages": [ + "@tmi-sent-ts=1726692469188;subscriber=1;id=4d2af478-c471-4b3a-8c6f-568a54d2fe7a;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :BLOCK" + ] + }, + "settings": { + "ignore": { + "phrases": [ + { + "caseSensitive": false, + "isBlock": false, + "pattern": "ignore", + "regex": false, + "replaceWith": "replace" + }, + { + "caseSensitive": true, + "isBlock": false, + "pattern": "CaseSensitive", + "regex": false, + "replaceWith": "casesensitivE" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "BLOCK", + "regex": false, + "replaceWith": "?" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "block!{2,}", + "regex": true, + "replaceWith": "replace" + } + ] + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/reply-blocked-user.json b/tests/snapshots/MessageBuilder/IRC/reply-blocked-user.json new file mode 100644 index 000000000..acd299367 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reply-blocked-user.json @@ -0,0 +1,187 @@ +{ + "input": "@tmi-sent-ts=1726694327762;subscriber=1;id=e5b84adf-b62d-47ff-8534-2cc57e24a5fb;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;reply-parent-msg-body=a;reply-thread-parent-user-login=blocked;reply-thread-parent-user-id=12345;reply-parent-user-login=blocked;reply-thread-parent-display-name=blocked;reply-parent-msg-id=ff82c584-5d20-459f-b2d5-dcacbb693559;reply-thread-parent-msg-id=ff82c584-5d20-459f-b2d5-dcacbb693559;reply-parent-display-name=blocked;reply-parent-user-id=12345 :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@nerixyz reply", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ReplyCurveElement" + }, + { + "color": "System", + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Replying", + "to" + ] + }, + { + "color": "Text", + "flags": "Text|RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "[Blocked", + "user]" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "21:18" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "21:18:47", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "fallbackColor": "Text", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "#ffff0000", + "userLoginName": "nerixyz", + "words": [ + "@nerixyz" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "reply" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "e5b84adf-b62d-47ff-8534-2cc57e24a5fb" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "e5b84adf-b62d-47ff-8534-2cc57e24a5fb", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "@nerixyz reply", + "searchText": "nerixyz nerixyz: @nerixyz reply ", + "serverReceivedTime": "2024-09-18T21:18:47Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "params": { + "prevMessages": [ + "@tmi-sent-ts=1726692855226;subscriber=1;id=ff82c584-5d20-459f-b2d5-dcacbb693559;room-id=11148817;user-id=12345;display-name=blocked;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :blocked!blocked@blocked.tmi.twitch.tv PRIVMSG #pajlada :a" + ] + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/reply-child.json b/tests/snapshots/MessageBuilder/IRC/reply-child.json new file mode 100644 index 000000000..8862f7738 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reply-child.json @@ -0,0 +1,192 @@ +{ + "input": "@tmi-sent-ts=1726690731517;subscriber=1;id=997cb8fa-4d24-411b-a5cd-433e515a5b72;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;reply-parent-display-name=nerixyz;reply-parent-user-login=nerixyz;reply-thread-parent-user-id=129546453;reply-parent-msg-body=@nerixyz\\sb;reply-parent-user-id=129546453;reply-thread-parent-display-name=nerixyz;reply-thread-parent-msg-id=72d76cb2-f34d-4a93-8005-61bf244456ee;reply-thread-parent-user-login=nerixyz;reply-parent-msg-id=474f19ab-a1b0-410a-877a-5b0e2ae8be6d :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@nerixyz c", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ReplyCurveElement" + }, + { + "color": "System", + "flags": "RepliedMessage", + "link": { + "type": "ViewThread", + "value": "72d76cb2-f34d-4a93-8005-61bf244456ee" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Replying", + "to" + ] + }, + { + "color": "#ffff0000", + "flags": "RepliedMessage", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "@nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text|RepliedMessage", + "link": { + "type": "ViewThread", + "value": "72d76cb2-f34d-4a93-8005-61bf244456ee" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "a" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:18" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:18:51", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "c" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ViewThread", + "value": "72d76cb2-f34d-4a93-8005-61bf244456ee" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|ReplyMessage", + "id": "997cb8fa-4d24-411b-a5cd-433e515a5b72", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "c", + "replyThread": { + "replies": [ + "474f19ab-a1b0-410a-877a-5b0e2ae8be6d", + "997cb8fa-4d24-411b-a5cd-433e515a5b72" + ], + "rootId": "72d76cb2-f34d-4a93-8005-61bf244456ee", + "subscription": "None" + }, + "searchText": "nerixyz nerixyz: c ", + "serverReceivedTime": "2024-09-18T20:18:51Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "params": { + "prevMessages": [ + "@tmi-sent-ts=1726690688294;subscriber=1;id=72d76cb2-f34d-4a93-8005-61bf244456ee;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :a", + "@tmi-sent-ts=1726690691139;subscriber=1;id=474f19ab-a1b0-410a-877a-5b0e2ae8be6d;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;reply-parent-user-login=nerixyz;reply-thread-parent-user-login=nerixyz;reply-parent-msg-body=a;reply-parent-user-id=129546453;reply-thread-parent-user-id=129546453;reply-thread-parent-display-name=nerixyz;reply-thread-parent-msg-id=72d76cb2-f34d-4a93-8005-61bf244456ee;reply-parent-msg-id=72d76cb2-f34d-4a93-8005-61bf244456ee;reply-parent-display-name=nerixyz :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@nerixyz b" + ] + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/reply-ignore.json b/tests/snapshots/MessageBuilder/IRC/reply-ignore.json new file mode 100644 index 000000000..f3d280877 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reply-ignore.json @@ -0,0 +1,225 @@ +{ + "input": "@tmi-sent-ts=1726692492710;subscriber=1;id=89c8f200-ae37-4279-909d-5ff4f9ed10a0;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;reply-parent-user-id=129546453;reply-parent-msg-body=ignore;reply-parent-display-name=nerixyz;reply-thread-parent-user-login=nerixyz;reply-parent-user-login=nerixyz;reply-thread-parent-display-name=nerixyz;reply-thread-parent-msg-id=68a68ef7-0ee3-4584-8937-6d20ff0a7a8a;reply-thread-parent-user-id=129546453;reply-parent-msg-id=68a68ef7-0ee3-4584-8937-6d20ff0a7a8a :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@nerixyz CaseSensitive", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ReplyCurveElement" + }, + { + "color": "System", + "flags": "RepliedMessage", + "link": { + "type": "ViewThread", + "value": "68a68ef7-0ee3-4584-8937-6d20ff0a7a8a" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Replying", + "to" + ] + }, + { + "color": "#ffff0000", + "flags": "RepliedMessage", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "@nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text|RepliedMessage", + "link": { + "type": "ViewThread", + "value": "68a68ef7-0ee3-4584-8937-6d20ff0a7a8a" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "replace" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:48" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:48:12", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "casesensitivE" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ViewThread", + "value": "68a68ef7-0ee3-4584-8937-6d20ff0a7a8a" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|ReplyMessage", + "id": "89c8f200-ae37-4279-909d-5ff4f9ed10a0", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "casesensitivE", + "replyParent": "68a68ef7-0ee3-4584-8937-6d20ff0a7a8a", + "replyThread": { + "replies": [ + "89c8f200-ae37-4279-909d-5ff4f9ed10a0" + ], + "rootId": "68a68ef7-0ee3-4584-8937-6d20ff0a7a8a", + "subscription": "None" + }, + "searchText": "nerixyz nerixyz: casesensitivE ", + "serverReceivedTime": "2024-09-18T20:48:12Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "params": { + "prevMessages": [ + "@tmi-sent-ts=1726692478291;subscriber=1;id=68a68ef7-0ee3-4584-8937-6d20ff0a7a8a;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :ignore" + ] + }, + "settings": { + "ignore": { + "phrases": [ + { + "caseSensitive": false, + "isBlock": false, + "pattern": "ignore", + "regex": false, + "replaceWith": "replace" + }, + { + "caseSensitive": true, + "isBlock": false, + "pattern": "CaseSensitive", + "regex": false, + "replaceWith": "casesensitivE" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "BLOCK", + "regex": false, + "replaceWith": "?" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "block!{2,}", + "regex": true, + "replaceWith": "replace" + } + ] + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/reply-no-prev.json b/tests/snapshots/MessageBuilder/IRC/reply-no-prev.json new file mode 100644 index 000000000..ca767517e --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reply-no-prev.json @@ -0,0 +1,178 @@ +{ + "input": "@tmi-sent-ts=1726690691139;subscriber=1;id=474f19ab-a1b0-410a-877a-5b0e2ae8be6d;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;reply-parent-user-login=nerixyz;reply-thread-parent-user-login=nerixyz;reply-parent-msg-body=a;reply-parent-user-id=129546453;reply-thread-parent-user-id=129546453;reply-thread-parent-display-name=nerixyz;reply-thread-parent-msg-id=72d76cb2-f34d-4a93-8005-61bf244456ee;reply-parent-msg-id=72d76cb2-f34d-4a93-8005-61bf244456ee;reply-parent-display-name=nerixyz :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@nerixyz b", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ReplyCurveElement" + }, + { + "color": "System", + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Replying", + "to" + ] + }, + { + "color": "Text", + "flags": "RepliedMessage", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "@nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text|RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "a" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:18" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:18:11", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "b" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "474f19ab-a1b0-410a-877a-5b0e2ae8be6d" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "474f19ab-a1b0-410a-877a-5b0e2ae8be6d", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "b", + "searchText": "nerixyz nerixyz: b ", + "serverReceivedTime": "2024-09-18T20:18:11Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/reply-root.json b/tests/snapshots/MessageBuilder/IRC/reply-root.json new file mode 100644 index 000000000..2e138c3d6 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reply-root.json @@ -0,0 +1,195 @@ +{ + "input": "@tmi-sent-ts=1726690741480;subscriber=1;id=f0b45994-3e92-4c37-a35f-062072163d9d;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;reply-parent-user-login=nerixyz;reply-thread-parent-display-name=nerixyz;reply-parent-display-name=nerixyz;reply-parent-msg-id=72d76cb2-f34d-4a93-8005-61bf244456ee;reply-thread-parent-msg-id=72d76cb2-f34d-4a93-8005-61bf244456ee;reply-thread-parent-user-id=129546453;reply-thread-parent-user-login=nerixyz;reply-parent-msg-body=a;reply-parent-user-id=129546453 :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@nerixyz d", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ReplyCurveElement" + }, + { + "color": "System", + "flags": "RepliedMessage", + "link": { + "type": "ViewThread", + "value": "72d76cb2-f34d-4a93-8005-61bf244456ee" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Replying", + "to" + ] + }, + { + "color": "#ffff0000", + "flags": "RepliedMessage", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "@nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text|RepliedMessage", + "link": { + "type": "ViewThread", + "value": "72d76cb2-f34d-4a93-8005-61bf244456ee" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "a" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:19" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:19:01", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "d" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ViewThread", + "value": "72d76cb2-f34d-4a93-8005-61bf244456ee" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|ReplyMessage", + "id": "f0b45994-3e92-4c37-a35f-062072163d9d", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "d", + "replyParent": "72d76cb2-f34d-4a93-8005-61bf244456ee", + "replyThread": { + "replies": [ + "474f19ab-a1b0-410a-877a-5b0e2ae8be6d", + "997cb8fa-4d24-411b-a5cd-433e515a5b72", + "f0b45994-3e92-4c37-a35f-062072163d9d" + ], + "rootId": "72d76cb2-f34d-4a93-8005-61bf244456ee", + "subscription": "None" + }, + "searchText": "nerixyz nerixyz: d ", + "serverReceivedTime": "2024-09-18T20:19:01Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "params": { + "prevMessages": [ + "@tmi-sent-ts=1726690688294;subscriber=1;id=72d76cb2-f34d-4a93-8005-61bf244456ee;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :a", + "@tmi-sent-ts=1726690691139;subscriber=1;id=474f19ab-a1b0-410a-877a-5b0e2ae8be6d;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;reply-parent-user-login=nerixyz;reply-thread-parent-user-login=nerixyz;reply-parent-msg-body=a;reply-parent-user-id=129546453;reply-thread-parent-user-id=129546453;reply-thread-parent-display-name=nerixyz;reply-thread-parent-msg-id=72d76cb2-f34d-4a93-8005-61bf244456ee;reply-parent-msg-id=72d76cb2-f34d-4a93-8005-61bf244456ee;reply-parent-display-name=nerixyz :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@nerixyz b", + "@tmi-sent-ts=1726690731517;subscriber=1;id=997cb8fa-4d24-411b-a5cd-433e515a5b72;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;reply-parent-display-name=nerixyz;reply-parent-user-login=nerixyz;reply-thread-parent-user-id=129546453;reply-parent-msg-body=@nerixyz\\sb;reply-parent-user-id=129546453;reply-thread-parent-display-name=nerixyz;reply-thread-parent-msg-id=72d76cb2-f34d-4a93-8005-61bf244456ee;reply-thread-parent-user-login=nerixyz;reply-parent-msg-id=474f19ab-a1b0-410a-877a-5b0e2ae8be6d :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@nerixyz c" + ] + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/reply-single.json b/tests/snapshots/MessageBuilder/IRC/reply-single.json new file mode 100644 index 000000000..2fbcf0964 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reply-single.json @@ -0,0 +1,310 @@ +{ + "input": "@tmi-sent-ts=1726690593888;subscriber=1;id=5e5f526a-5d60-4d81-800b-3b81b8a34c2c;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=1902:11-15;reply-parent-display-name=nerixyz;reply-thread-parent-msg-id=d95d04a6-5c6a-476a-8dbd-7c6d3b3c277f;reply-thread-parent-user-login=nerixyz;reply-thread-parent-display-name=nerixyz;reply-parent-msg-body=a\\sKappa\\sBTTVEmote\\s😂\\sb;reply-parent-user-login=nerixyz;reply-parent-user-id=129546453;reply-thread-parent-user-id=129546453;reply-parent-msg-id=d95d04a6-5c6a-476a-8dbd-7c6d3b3c277f :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :@nerixyz c Keepo FFZEmote 😭 d", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ReplyCurveElement" + }, + { + "color": "System", + "flags": "RepliedMessage", + "link": { + "type": "ViewThread", + "value": "d95d04a6-5c6a-476a-8dbd-7c6d3b3c277f" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Replying", + "to" + ] + }, + { + "color": "#ffff0000", + "flags": "RepliedMessage", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "@nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text|RepliedMessage", + "link": { + "type": "ViewThread", + "value": "d95d04a6-5c6a-476a-8dbd-7c6d3b3c277f" + }, + "style": "ChatMediumSmall", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "a", + "Kappa", + "BTTVEmote", + "😂", + "b" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:16" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:16:33", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "c" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/1902/default/dark/3.0" + }, + "name": "Keepo", + "tooltip": "Keepo
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Keepo" + ] + }, + "tooltip": "Keepo
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/FFZEmote", + "images": { + "1x": "https://chatterino.com/FFZEmote.png" + }, + "name": "FFZEmote", + "tooltip": "FFZEmote Tooltip" + }, + "flags": "FfzEmoteImage|FfzEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "FFZEmote" + ] + }, + "tooltip": "FFZEmote Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://pajbot.com/static/emoji-v2/img/twitter/64/1f62d.png" + }, + "name": "😭", + "tooltip": ":sob:
Emoji" + }, + "flags": "EmojiImage|EmojiText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "😭" + ] + }, + "tooltip": ":sob:
Emoji", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "d" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ViewThread", + "value": "d95d04a6-5c6a-476a-8dbd-7c6d3b3c277f" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|ReplyMessage", + "id": "5e5f526a-5d60-4d81-800b-3b81b8a34c2c", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "c Keepo FFZEmote 😭 d", + "replyParent": "d95d04a6-5c6a-476a-8dbd-7c6d3b3c277f", + "replyThread": { + "replies": [ + "5e5f526a-5d60-4d81-800b-3b81b8a34c2c" + ], + "rootId": "d95d04a6-5c6a-476a-8dbd-7c6d3b3c277f", + "subscription": "None" + }, + "searchText": "nerixyz nerixyz: c Keepo FFZEmote 😭 d ", + "serverReceivedTime": "2024-09-18T20:16:33Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "params": { + "prevMessages": [ + "@tmi-sent-ts=1726690560784;subscriber=1;id=d95d04a6-5c6a-476a-8dbd-7c6d3b3c277f;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=25:2-6 :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :a Kappa BTTVEmote 😂 b" + ] + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/reward-bits.json b/tests/snapshots/MessageBuilder/IRC/reward-bits.json new file mode 100644 index 000000000..8ad2e447d --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reward-bits.json @@ -0,0 +1,305 @@ +{ + "input": "@tmi-sent-ts=1726662938032;subscriber=1;id=87af876f-5591-4a63-924f-46f465ecd3c4;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;custom-reward-id=CELEBRATION :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :reward 1", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:00" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:00:00", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "Text", + "flags": "ChannelPointReward", + "link": { + "type": "UserInfo", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "" + ] + }, + { + "color": "Text", + "flags": "ChannelPointReward", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "redeemed" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/42/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/42/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/42/default/dark/3.0" + }, + "name": "MyBitsEmote", + "tooltip": "MyBitsEmote
Twitch Emote" + }, + "flags": "ChannelPointReward", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "MyBitsEmote" + ] + }, + "tooltip": "MyBitsEmote
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "ChannelPointReward", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "On-Screen", + "Celebration" + ] + }, + { + "flags": "TwitchEmoteImage|ChannelPointReward", + "image": "https://chatterino.com/reward1x.png", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ScalingImageElement" + }, + { + "color": "Text", + "flags": "ChannelPointReward", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "color": "Text", + "flags": "ChannelPointReward", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "bits" + ] + }, + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "12:35" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "12:35:38", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "reward" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "87af876f-5591-4a63-924f-46f465ecd3c4" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|RedeemedChannelPointReward", + "id": "87af876f-5591-4a63-924f-46f465ecd3c4", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "reward 1", + "reward": { + "channelId": "11148817", + "cost": 1, + "emoteId": "42", + "emoteName": "MyBitsEmote", + "id": "CELEBRATION", + "image": { + "1x": "https://chatterino.com/reward1x.png", + "2x": "https://chatterino.com/reward2x.png", + "3x": "https://chatterino.com/reward4x.png" + }, + "isBits": true, + "isUserInputRequired": false, + "title": "On-Screen Celebration", + "user": { + "displayName": "", + "id": "", + "login": "" + } + }, + "searchText": "nerixyz nerixyz: reward 1 redeemed On-Screen Celebration 1", + "serverReceivedTime": "2024-09-18T12:35:38Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/reward-blocked-user.json b/tests/snapshots/MessageBuilder/IRC/reward-blocked-user.json new file mode 100644 index 000000000..5aa2fb015 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reward-blocked-user.json @@ -0,0 +1,5 @@ +{ + "input": "@tmi-sent-ts=1728127030336;subscriber=1;id=d09d1034-4f73-422c-b239-29d463a47973;room-id=11148817;user-id=12345;display-name=blocked;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=25:11-15;custom-reward-id=47cc9d27-f771-47d9-8fb9-893b8524ccb3 :blocked!blocked@blocked.tmi.twitch.tv PRIVMSG #pajlada :test TESTS Kappa", + "output": [ + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/reward-empty.json b/tests/snapshots/MessageBuilder/IRC/reward-empty.json new file mode 100644 index 000000000..b9a9f92b4 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reward-empty.json @@ -0,0 +1,240 @@ +{ + "input": "@tmi-sent-ts=1726677572248;subscriber=1;id=2932e250-1f49-4587-8783-7a13d9f99f6b;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;custom-reward-id=dc8d1dac-256e-42b9-b7ba-40b32e5294e2 :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :empty", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:00" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:00:00", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "Text", + "flags": "ChannelPointReward", + "link": { + "type": "UserInfo", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "" + ] + }, + { + "color": "Text", + "flags": "ChannelPointReward", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "redeemed" + ] + }, + { + "color": "Text", + "flags": "ChannelPointReward", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "test" + ] + }, + { + "flags": "TwitchEmoteImage|ChannelPointReward", + "image": "https://chatterino.com/reward1x.png", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ScalingImageElement" + }, + { + "color": "Text", + "flags": "ChannelPointReward", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "16:39" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "16:39:32", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "empty" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "2932e250-1f49-4587-8783-7a13d9f99f6b" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|RedeemedChannelPointReward", + "id": "2932e250-1f49-4587-8783-7a13d9f99f6b", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "empty", + "reward": { + "channelId": "11148817", + "cost": 1, + "emoteId": "", + "emoteName": "", + "id": "dc8d1dac-256e-42b9-b7ba-40b32e5294e2", + "image": { + "1x": "https://chatterino.com/reward1x.png", + "2x": "https://chatterino.com/reward2x.png", + "3x": "https://chatterino.com/reward4x.png" + }, + "isBits": false, + "isUserInputRequired": false, + "title": "test", + "user": { + "displayName": "", + "id": "", + "login": "" + } + }, + "searchText": "nerixyz nerixyz: empty redeemed test 1", + "serverReceivedTime": "2024-09-18T16:39:32Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/reward-known.json b/tests/snapshots/MessageBuilder/IRC/reward-known.json new file mode 100644 index 000000000..1be46d8cf --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reward-known.json @@ -0,0 +1,251 @@ +{ + "input": "@tmi-sent-ts=1726662938032;subscriber=1;id=87af876f-5591-4a63-924f-46f465ecd3c4;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;custom-reward-id=31a2344e-0fce-4229-9453-fb2e8b6dd02c :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :reward 1", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:00" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:00:00", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "Text", + "flags": "ChannelPointReward", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Redeemed" + ] + }, + { + "color": "Text", + "flags": "ChannelPointReward", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "my", + "reward" + ] + }, + { + "flags": "TwitchEmoteImage|ChannelPointReward", + "image": "https://chatterino.com/reward1x.png", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ScalingImageElement" + }, + { + "color": "Text", + "flags": "ChannelPointReward", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "flags": "ChannelPointReward", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "LinebreakElement" + }, + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "12:35" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "12:35:38", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "reward" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "87af876f-5591-4a63-924f-46f465ecd3c4" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|RedeemedChannelPointReward", + "id": "87af876f-5591-4a63-924f-46f465ecd3c4", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "reward 1", + "reward": { + "channelId": "11148817", + "cost": 1, + "emoteId": "", + "emoteName": "", + "id": "31a2344e-0fce-4229-9453-fb2e8b6dd02c", + "image": { + "1x": "https://chatterino.com/reward1x.png", + "2x": "https://chatterino.com/reward2x.png", + "3x": "https://chatterino.com/reward4x.png" + }, + "isBits": false, + "isUserInputRequired": true, + "title": "my reward", + "user": { + "displayName": "", + "id": "", + "login": "" + } + }, + "searchText": "nerixyz nerixyz: reward 1 Redeemed my reward 1", + "serverReceivedTime": "2024-09-18T12:35:38Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/reward-unknown.json b/tests/snapshots/MessageBuilder/IRC/reward-unknown.json new file mode 100644 index 000000000..d614dd1fa --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/reward-unknown.json @@ -0,0 +1,137 @@ +{ + "input": "@tmi-sent-ts=1726662961590;subscriber=1;id=4d409401-f692-4dd1-9295-f21350f86860;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=;custom-reward-id=3a02deb3-626e-4d8e-adde-226c90bbbb84 :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :reward 2", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "12:36" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "12:36:01", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "reward" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "4d409401-f692-4dd1-9295-f21350f86860" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "4d409401-f692-4dd1-9295-f21350f86860", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "reward 2", + "searchText": "nerixyz nerixyz: reward 2 ", + "serverReceivedTime": "2024-09-18T12:36:01Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/rm-deleted.json b/tests/snapshots/MessageBuilder/IRC/rm-deleted.json new file mode 100644 index 000000000..343dee792 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/rm-deleted.json @@ -0,0 +1,273 @@ +{ + "input": "@id=3dc39240-0798-4103-8c3c-51e1a9a567a3;first-msg=0;historical=1;rm-received-ts=1726600627161;color=#FF0000;badges=subscriber/24;turbo=0;room-id=11148817;flags=;mod=0;returning-chatter=0;rm-deleted=1;emotes=25:9-13;tmi-sent-ts=1726600626982;subscriber=1;user-id=129546453;display-name=nerixyz;badge-info=subscriber/27;user-type= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :7TVEmote Kappa my7TVEmote hi 7TVEmote❤", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "19:17" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "19:17:07", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/7TVEmote", + "id": "1", + "images": { + "1x": "https://chatterino.com/7TVEmote.png" + }, + "name": "7TVEmote", + "tooltip": "7TVEmote Tooltip" + }, + "flags": "SevenTVEmoteImage|SevenTVEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "7TVEmote" + ] + }, + "tooltip": "7TVEmote Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "my7TVEmote" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "hi" + ] + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/7TVEmote", + "id": "1", + "images": { + "1x": "https://chatterino.com/7TVEmote.png" + }, + "name": "7TVEmote", + "tooltip": "7TVEmote Tooltip" + }, + "flags": "SevenTVEmoteImage|SevenTVEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "7TVEmote" + ] + }, + "tooltip": "7TVEmote Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "images": { + "1x": "https://pajbot.com/static/emoji-v2/img/twitter/64/2764-fe0f.png" + }, + "name": "❤️", + "tooltip": ":heart:
Emoji" + }, + "flags": "EmojiImage|EmojiText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "❤️" + ] + }, + "tooltip": ":heart:
Emoji", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "3dc39240-0798-4103-8c3c-51e1a9a567a3" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Disabled|Collapsed", + "id": "3dc39240-0798-4103-8c3c-51e1a9a567a3", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "7TVEmote Kappa my7TVEmote hi 7TVEmote❤", + "searchText": "nerixyz nerixyz: 7TVEmote Kappa my7TVEmote hi 7TVEmote❤ ", + "serverReceivedTime": "2024-09-17T19:17:07Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/shared-chat-emotes.json b/tests/snapshots/MessageBuilder/IRC/shared-chat-emotes.json new file mode 100644 index 000000000..b7f9256d4 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/shared-chat-emotes.json @@ -0,0 +1,392 @@ +{ + "input": "@tmi-sent-ts=1728311235904;subscriber=1;id=5b6f9721-80fd-4036-951e-1ced9c591b32;room-id=11148817;user-id=129546453;display-name=nerixyz;source-badge-info=;source-room-id=141981764;source-id=d7be8e12-187c-4f96-9d96-d87a17aa9ce9;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=;emotes=25:0-4 :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :Kappa BTTVEmote BTTVGlobal 7TVEmote 7TVGlobal FFZEmote FFZGlobal BTTVTwitchDev 7TVTwitchDev FFZTwitchDev", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "14:27" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "14:27:15", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "BTTVEmote" + ] + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/BTTVGlobal", + "images": { + "1x": "https://chatterino.com/BTTVGlobal.png" + }, + "name": "BTTVGlobal", + "tooltip": "BTTVGlobal Tooltip" + }, + "flags": "BttvEmoteImage|BttvEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "BTTVGlobal" + ] + }, + "tooltip": "BTTVGlobal Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "7TVEmote" + ] + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/7TVGlobal", + "id": "G1", + "images": { + "1x": "https://chatterino.com/7TVGlobal.png" + }, + "name": "7TVGlobal", + "tooltip": "7TVGlobal Tooltip" + }, + "flags": "SevenTVEmoteImage|SevenTVEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "7TVGlobal" + ] + }, + "tooltip": "7TVGlobal Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "FFZEmote" + ] + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/FFZGlobal", + "images": { + "1x": "https://chatterino.com/FFZGlobal.png" + }, + "name": "FFZGlobal", + "tooltip": "FFZGlobal Tooltip" + }, + "flags": "FfzEmoteImage|FfzEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "FFZGlobal" + ] + }, + "tooltip": "FFZGlobal Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/BTTVTwitchDev", + "images": { + "1x": "https://chatterino.com/BTTVTwitchDev.png" + }, + "name": "BTTVTwitchDev", + "tooltip": "BTTVTwitchDev Tooltip" + }, + "flags": "BttvEmoteImage|BttvEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "BTTVTwitchDev" + ] + }, + "tooltip": "BTTVTwitchDev Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/7TVTwitchDev", + "id": "t5", + "images": { + "1x": "https://chatterino.com/7TVTwitchDev.png" + }, + "name": "7TVTwitchDev", + "tooltip": "7TVTwitchDev Tooltip" + }, + "flags": "SevenTVEmoteImage|SevenTVEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "7TVTwitchDev" + ] + }, + "tooltip": "7TVTwitchDev Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "emote": { + "author": "Chatterino", + "homePage": "https://chatterino.com/FFZTwitchDev", + "images": { + "1x": "https://chatterino.com/FFZTwitchDev.png" + }, + "name": "FFZTwitchDev", + "tooltip": "FFZTwitchDev Tooltip" + }, + "flags": "FfzEmoteImage|FfzEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "FFZTwitchDev" + ] + }, + "tooltip": "FFZTwitchDev Tooltip", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "5b6f9721-80fd-4036-951e-1ced9c591b32" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "DoNotTriggerNotification|Collapsed|SharedMessage", + "id": "5b6f9721-80fd-4036-951e-1ced9c591b32", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "Kappa BTTVEmote BTTVGlobal 7TVEmote 7TVGlobal FFZEmote FFZGlobal BTTVTwitchDev 7TVTwitchDev FFZTwitchDev", + "searchText": "nerixyz nerixyz: Kappa BTTVEmote BTTVGlobal 7TVEmote 7TVGlobal FFZEmote FFZGlobal BTTVTwitchDev 7TVTwitchDev FFZTwitchDev ", + "serverReceivedTime": "2024-10-07T14:27:15Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/shared-chat-known.json b/tests/snapshots/MessageBuilder/IRC/shared-chat-known.json new file mode 100644 index 000000000..6a13adcb4 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/shared-chat-known.json @@ -0,0 +1,161 @@ +{ + "input": "@badge-info=;flags=;room-id=11148817;color=;client-nonce=0d1632f37b6baee51576859d5dbaf325;emotes=;subscriber=0;tmi-sent-ts=1727395701680;id=19ee1663-c14d-41cd-a4a2-30a4bb609c5a;turbo=1;badges=staff/1,turbo/1;source-badges=staff/1,moderator/1,bits-leader/1;source-badge-info=;display-name=creativewind;source-room-id=141981764;source-id=b97eea45-f9dc-4f0c-8744-f8256c3ed950;user-type=staff;user-id=106940612;mod=0 :creativewind!creativewind@creativewind.tmi.twitch.tv PRIVMSG #pajlada :Guy", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "staff", + "turbo" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "creativewind", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:08" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:08:21", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "homePage": "https://www.twitch.tv/jobs?ref=chat_badge", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/3" + }, + "name": "", + "tooltip": "Staff" + }, + "flags": "BadgeGlobalAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Staff", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/3" + }, + "name": "", + "tooltip": "Turbo" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Turbo", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff00ff00", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "creativewind" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "creativewind:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Guy" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "19ee1663-c14d-41cd-a4a2-30a4bb609c5a" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "DoNotTriggerNotification|Collapsed|SharedMessage", + "id": "19ee1663-c14d-41cd-a4a2-30a4bb609c5a", + "localizedName": "", + "loginName": "creativewind", + "messageText": "Guy", + "searchText": "creativewind creativewind: Guy ", + "serverReceivedTime": "2024-09-27T00:08:21Z", + "timeoutUser": "", + "usernameColor": "#ff00ff00" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/shared-chat-same-channel.json b/tests/snapshots/MessageBuilder/IRC/shared-chat-same-channel.json new file mode 100644 index 000000000..5ab3daad0 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/shared-chat-same-channel.json @@ -0,0 +1,161 @@ +{ + "input": "@badge-info=;flags=;room-id=11148817;color=;client-nonce=0d1632f37b6baee51576859d5dbaf325;emotes=;subscriber=0;tmi-sent-ts=1727395701680;id=19ee1663-c14d-41cd-a4a2-30a4bb609c5a;turbo=1;badges=staff/1,turbo/1;source-badges=staff/1,moderator/1,bits-leader/1;source-badge-info=;display-name=creativewind;source-room-id=11148817;source-id=19ee1663-c14d-41cd-a4a2-30a4bb609c5a;user-type=staff;user-id=106940612;mod=0 :creativewind!creativewind@creativewind.tmi.twitch.tv PRIVMSG #pajlada :Guys", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "staff", + "turbo" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "creativewind", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:08" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:08:21", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "homePage": "https://www.twitch.tv/jobs?ref=chat_badge", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/3" + }, + "name": "", + "tooltip": "Staff" + }, + "flags": "BadgeGlobalAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Staff", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/3" + }, + "name": "", + "tooltip": "Turbo" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Turbo", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff00ff00", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "creativewind" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "creativewind:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Guys" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "19ee1663-c14d-41cd-a4a2-30a4bb609c5a" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "19ee1663-c14d-41cd-a4a2-30a4bb609c5a", + "localizedName": "", + "loginName": "creativewind", + "messageText": "Guys", + "searchText": "creativewind creativewind: Guys ", + "serverReceivedTime": "2024-09-27T00:08:21Z", + "timeoutUser": "", + "usernameColor": "#ff00ff00" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/shared-chat-unknown.json b/tests/snapshots/MessageBuilder/IRC/shared-chat-unknown.json new file mode 100644 index 000000000..accbb76b0 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/shared-chat-unknown.json @@ -0,0 +1,161 @@ +{ + "input": "@badge-info=;flags=;room-id=11148817;color=;client-nonce=0d1632f37b6baee51576859d5dbaf325;emotes=;subscriber=0;tmi-sent-ts=1727395701680;id=19ee1663-c14d-41cd-a4a2-30a4bb609c5a;turbo=1;badges=staff/1,turbo/1;source-badges=staff/1,moderator/1,bits-leader/1;source-badge-info=;display-name=creativewind;source-room-id=1025594235;source-id=b97eea45-f9dc-4f0c-8744-f8256c3ed950;user-type=staff;user-id=106940612;mod=0 :creativewind!creativewind@creativewind.tmi.twitch.tv PRIVMSG #pajlada :Guys", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "staff", + "turbo" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "creativewind", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:08" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:08:21", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "homePage": "https://www.twitch.tv/jobs?ref=chat_badge", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/3" + }, + "name": "", + "tooltip": "Staff" + }, + "flags": "BadgeGlobalAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Staff", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/3" + }, + "name": "", + "tooltip": "Turbo" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Turbo", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff00ff00", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "creativewind" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "creativewind:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Guys" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "19ee1663-c14d-41cd-a4a2-30a4bb609c5a" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|SharedMessage", + "id": "19ee1663-c14d-41cd-a4a2-30a4bb609c5a", + "localizedName": "", + "loginName": "creativewind", + "messageText": "Guys", + "searchText": "creativewind creativewind: Guys ", + "serverReceivedTime": "2024-09-27T00:08:21Z", + "timeoutUser": "", + "usernameColor": "#ff00ff00" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/simple.json b/tests/snapshots/MessageBuilder/IRC/simple.json new file mode 100644 index 000000000..1ce363b9a --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/simple.json @@ -0,0 +1,161 @@ +{ + "input": "@badge-info=subscriber/17;badges=subscriber/12,no_audio/1;color=#EBA2C0;display-name=jammehcow;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=9c2dd916-5a6d-4c1f-9fe7-a081b62a9c6b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662201093248;turbo=0;user-id=82674227;user-type= :jammehcow!jammehcow@jammehcow.tmi.twitch.tv PRIVMSG #pajlada :Kappa", + "output": [ + { + "badgeInfos": { + "subscriber": "17" + }, + "badges": [ + "subscriber", + "no_audio" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "jammehcow", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:31" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:31:33", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/3" + }, + "name": "", + "tooltip": "Watching without audio" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Watching without audio", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffeba2c0", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "jammehcow" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "jammehcow:" + ] + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/1.0", + "2x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0", + "3x": "https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/3.0" + }, + "name": "Kappa", + "tooltip": "Kappa
Twitch Emote" + }, + "flags": "TwitchEmoteImage|TwitchEmoteText", + "link": { + "type": "None", + "value": "" + }, + "text": { + "color": "Text", + "flags": "Misc", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + }, + "tooltip": "Kappa
Twitch Emote", + "trailingSpace": true, + "type": "EmoteElement" + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "9c2dd916-5a6d-4c1f-9fe7-a081b62a9c6b" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "9c2dd916-5a6d-4c1f-9fe7-a081b62a9c6b", + "localizedName": "", + "loginName": "jammehcow", + "messageText": "Kappa", + "searchText": "jammehcow jammehcow: Kappa ", + "serverReceivedTime": "2022-09-03T10:31:33Z", + "timeoutUser": "", + "usernameColor": "#ffeba2c0" + } + ] +} diff --git a/tests/snapshots/MessageBuilder/IRC/username-localized.json b/tests/snapshots/MessageBuilder/IRC/username-localized.json new file mode 100644 index 000000000..8c78475d3 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/username-localized.json @@ -0,0 +1,119 @@ +{ + "input": "@tmi-sent-ts=1726916057144;subscriber=1;id=3ad26770-7299-4261-ab9b-24f410944517;room-id=11148817;user-id=117166826;display-name=display-name=테스트계정420;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=mod;emotes= :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :username", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "testaccount_420", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:54" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:54:17", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "testaccount_420" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "display-name=테스트계정420:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "username" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "3ad26770-7299-4261-ab9b-24f410944517" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "3ad26770-7299-4261-ab9b-24f410944517", + "localizedName": "display-name=테스트계정420", + "loginName": "testaccount_420", + "messageText": "username", + "searchText": "display-name=테스트계정420 display-name=테스트계정420 testaccount_420: username ", + "serverReceivedTime": "2024-09-21T10:54:17Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "settings": { + "appearance": { + "messages": { + "usernameDisplayMode": 2 + } + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/username-localized2.json b/tests/snapshots/MessageBuilder/IRC/username-localized2.json new file mode 100644 index 000000000..66cbe408d --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/username-localized2.json @@ -0,0 +1,119 @@ +{ + "input": "@tmi-sent-ts=1726916057144;subscriber=1;id=3ad26770-7299-4261-ab9b-24f410944517;room-id=11148817;user-id=117166826;display-name=testaccount_420;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=mod;emotes= :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :username", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "testaccount_420", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:54" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:54:17", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "testaccount_420" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "testaccount_420:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "username" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "3ad26770-7299-4261-ab9b-24f410944517" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "3ad26770-7299-4261-ab9b-24f410944517", + "localizedName": "", + "loginName": "testaccount_420", + "messageText": "username", + "searchText": "testaccount_420 testaccount_420: username ", + "serverReceivedTime": "2024-09-21T10:54:17Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "settings": { + "appearance": { + "messages": { + "usernameDisplayMode": 2 + } + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/username.json b/tests/snapshots/MessageBuilder/IRC/username.json new file mode 100644 index 000000000..9076136a8 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/username.json @@ -0,0 +1,119 @@ +{ + "input": "@tmi-sent-ts=1726916057144;subscriber=1;id=3ad26770-7299-4261-ab9b-24f410944517;room-id=11148817;user-id=117166826;display-name=display-name=테스트계정420;badges=subscriber/24;badge-info=subscriber/27;color=#FF0000;flags=;user-type=mod;emotes= :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :username", + "output": [ + { + "badgeInfos": { + "subscriber": "27" + }, + "badges": [ + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "testaccount_420", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "10:54" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "10:54:17", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "testaccount_420" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "testaccount_420:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "username" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "3ad26770-7299-4261-ab9b-24f410944517" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "3ad26770-7299-4261-ab9b-24f410944517", + "localizedName": "display-name=테스트계정420", + "loginName": "testaccount_420", + "messageText": "username", + "searchText": "testaccount_420 display-name=테스트계정420 testaccount_420: username ", + "serverReceivedTime": "2024-09-21T10:54:17Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ], + "settings": { + "appearance": { + "messages": { + "usernameDisplayMode": 1 + } + } + } +} diff --git a/tests/snapshots/MessageBuilder/IRC/vip.json b/tests/snapshots/MessageBuilder/IRC/vip.json new file mode 100644 index 000000000..e7cac5680 --- /dev/null +++ b/tests/snapshots/MessageBuilder/IRC/vip.json @@ -0,0 +1,143 @@ +{ + "input": "@tmi-sent-ts=1726920321214;subscriber=1;vip=1;id=97bb0dfb-a35f-446d-8634-7522d8ef73ed;room-id=11148817;user-id=129546453;display-name=nerixyz;badges=vip/1,subscriber/48;badge-info=subscriber/64;color=#FF0000;flags=;user-type=;emotes= :nerixyz!nerixyz@nerixyz.tmi.twitch.tv PRIVMSG #pajlada :a", + "output": [ + { + "badgeInfos": { + "subscriber": "64" + }, + "badges": [ + "vip", + "subscriber" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "nerixyz", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "12:05" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "12:05:21", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "homePage": "https://help.twitch.tv/customer/en/portal/articles/659115-twitch-chat-badges-guide", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/3" + }, + "name": "", + "tooltip": "VIP" + }, + "flags": "BadgeChannelAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "VIP", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "97bb0dfb-a35f-446d-8634-7522d8ef73ed" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed", + "id": "97bb0dfb-a35f-446d-8634-7522d8ef73ed", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "a", + "searchText": "nerixyz nerixyz: a ", + "serverReceivedTime": "2024-09-21T12:05:21Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + } + ] +} diff --git a/tests/src/MessageBuilder.cpp b/tests/src/MessageBuilder.cpp index a51a93b62..b76d78018 100644 --- a/tests/src/MessageBuilder.cpp +++ b/tests/src/MessageBuilder.cpp @@ -1,34 +1,63 @@ #include "messages/MessageBuilder.hpp" +#include "common/Literals.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/highlights/HighlightController.hpp" #include "controllers/ignores/IgnorePhrase.hpp" +#include "controllers/sound/NullBackend.hpp" +#include "lib/Snapshot.hpp" +#include "messages/Emote.hpp" +#include "messages/Message.hpp" #include "mocks/BaseApplication.hpp" -#include "mocks/Channel.hpp" #include "mocks/ChatterinoBadges.hpp" #include "mocks/DisabledStreamerMode.hpp" #include "mocks/Emotes.hpp" +#include "mocks/LinkResolver.hpp" #include "mocks/Logging.hpp" #include "mocks/TwitchIrcServer.hpp" #include "mocks/UserData.hpp" #include "providers/ffz/FfzBadges.hpp" #include "providers/seventv/SeventvBadges.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/ChannelPointReward.hpp" +#include "providers/twitch/IrcMessageHandler.hpp" +#include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchBadge.hpp" +#include "providers/twitch/TwitchBadges.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "singletons/Emotes.hpp" #include "Test.hpp" #include "util/IrcHelpers.hpp" #include #include +#include +#include +#include +#include +#include +#include #include #include #include using namespace chatterino; -using chatterino::mock::MockChannel; +using namespace literals; namespace { +/// Controls whether snapshots will be updated (true) or verified (false) +/// +/// In CI, all snapshots must be verified, thus the integrity tests checks for +/// this constant. +/// +/// When adding a test, start with `{ "input": "..." }` and set this to `true` +/// to generate an initial snapshot. Make sure to verify the output! +constexpr bool UPDATE_SNAPSHOTS = false; + +const QString IRC_CATEGORY = u"MessageBuilder/IRC"_s; + class MockApplication : public mock::BaseApplication { public: @@ -37,6 +66,12 @@ public: { } + MockApplication(const QString &settingsData) + : mock::BaseApplication(settingsData) + , highlights(this->settings, &this->accounts) + { + } + IEmotes *getEmotes() override { return &this->emotes; @@ -97,6 +132,21 @@ public: return &this->logging; } + TwitchBadges *getTwitchBadges() override + { + return &this->twitchBadges; + } + + ILinkResolver *getLinkResolver() override + { + return &this->linkResolver; + } + + ISoundController *getSound() override + { + return &this->sound; + } + mock::EmptyLogging logging; AccountController accounts; mock::Emotes emotes; @@ -109,8 +159,292 @@ public: BttvEmotes bttvEmotes; FfzEmotes ffzEmotes; SeventvEmotes seventvEmotes; + TwitchBadges twitchBadges; + mock::EmptyLinkResolver linkResolver; + NullBackend sound; }; +std::pair makeEmote(Emote &&emote) +{ + auto ptr = std::make_shared(std::move(emote)); + ptr->homePage = {u"https://chatterino.com/" % ptr->name.string}; + ptr->tooltip = {ptr->name.string % u" Tooltip"_s}; + ptr->author = {u"Chatterino"_s}; + ptr->images = { + Url{u"https://chatterino.com/" % ptr->name.string % u".png"}}; + return {ptr->name, ptr}; +} + +using EmoteMapPtr = std::shared_ptr; + +EmoteMapPtr makeEmotes(auto &&...emotes) +{ + auto map = std::make_shared(); + ((map->emplace(makeEmote(std::forward(emotes)))), ...); + return map; +} + +QT_WARNING_PUSH +QT_WARNING_DISABLE_CLANG("-Wmissing-field-initializers") + +struct MockEmotes { + EmoteMapPtr seventv; + EmoteMapPtr bttv; + EmoteMapPtr ffz; + EmoteMapPtr twitchAccount; + + static MockEmotes channel() + { + return { + .seventv = makeEmotes( + Emote{ + .name = {u"7TVEmote"_s}, + .id = {u"1"_s}, + }, + Emote{ + .name = {u"7TVEmote0w"_s}, + .zeroWidth = true, + .id = {u"2"_s}, + .baseName = EmoteName{u"ZeroWidth"_s}, + }, + Emote{ + .name = {u"PogChamp"_s}, + .id = {u"3"_s}, + }), + .bttv = makeEmotes( + Emote{ + .name = {u"BTTVEmote"_s}, + }, + Emote{ + .name = {u"Kappa"_s}, + }), + .ffz = makeEmotes( + Emote{ + .name = {u"FFZEmote"_s}, + }, + Emote{ + .name = {u"Keepo"_s}, + }), + }; + } + + static MockEmotes twitchdev() + { + return { + .seventv = makeEmotes(Emote{ + .name = {u"7TVTwitchDev"_s}, + .id = {u"t5"_s}, + }), + .bttv = makeEmotes(Emote{ + .name = {u"BTTVTwitchDev"_s}, + }), + .ffz = makeEmotes(Emote{ + .name = {u"FFZTwitchDev"_s}, + }), + }; + } + + static MockEmotes global() + { + return { + .seventv = makeEmotes(Emote{ + .name = {u"7TVGlobal"_s}, + .id = {u"G1"_s}, + }), + .bttv = makeEmotes(Emote{ + .name = {u"BTTVGlobal"_s}, + }), + .ffz = makeEmotes(Emote{ + .name = {u"FFZGlobal"_s}, + }), + .twitchAccount = makeEmotes(Emote{ + .name = {u"MyCoolTwitchEmote"_s}, + .id = {u"5678"_s}, + }), + }; + } +}; + +const QByteArray CHEERMOTE_JSON{R"({ + "prefix": "Cheer", + "tiers": [ + { + "min_bits": 1, + "id": "1", + "color": "#979797", + "images": { + "dark": { + "animated": { + "1": "https://chatterino.com/bits/1.gif", + "2": "https://chatterino.com/bits/2.gif", + "4": "https://chatterino.com/bits/4.gif" + }, + "static": { + "1": "https://chatterino.com/bits/1.png", + "2": "https://chatterino.com/bits/2.png", + "4": "https://chatterino.com/bits/4.png" + } + } + }, + "can_cheer": true, + "show_in_bits_card": true + }, + { + "min_bits": 100, + "id": "100", + "color": "#9c3ee8", + "images": { + "dark": { + "animated": { + "1": "https://chatterino.com/bits/1.gif", + "2": "https://chatterino.com/bits/2.gif", + "4": "https://chatterino.com/bits/4.gif" + }, + "static": { + "1": "https://chatterino.com/bits/1.png", + "2": "https://chatterino.com/bits/2.png", + "4": "https://chatterino.com/bits/4.png" + } + } + }, + "can_cheer": true, + "show_in_bits_card": true + } + ], + "type": "global_first_party", + "order": 1, + "last_updated": "2018-05-22T00:06:04Z", + "is_charitable": false +})"_ba}; + +const QByteArray LOCAL_BADGE_JSON{R"({ + "data": [ + { + "set_id": "subscriber", + "versions": [ + { + "click_url": null, + "description": "Subscriber", + "id": "3072", + "image_url_1x": "https://chatterino.com/tb-1", + "image_url_2x": "https://chatterino.com/tb-2", + "image_url_4x": "https://chatterino.com/tb-3", + "title": "Subscriber" + } + ] + } + ] +})"_ba}; + +const QByteArray SETTINGS_DEFAULT{"{}"_ba}; + +std::shared_ptr makeMockTwitchChannel( + const QString &name, const testlib::Snapshot &snapshot) +{ + auto chan = std::make_shared(name); + auto mocks = MockEmotes::channel(); + chan->setSeventvEmotes(std::move(mocks.seventv)); + chan->setBttvEmotes(std::move(mocks.bttv)); + chan->setFfzEmotes(std::move(mocks.ffz)); + + QJsonObject defaultImage{ + {u"url_1x"_s, u"https://chatterino.com/reward1x.png"_s}, + {u"url_2x"_s, u"https://chatterino.com/reward2x.png"_s}, + {u"url_4x"_s, u"https://chatterino.com/reward4x.png"_s}, + }; + chan->addKnownChannelPointReward({{ + {u"channel_id"_s, u"11148817"_s}, + {u"id"_s, u"unused"_s}, + {u"reward"_s, + {{ + {u"channel_id"_s, u"11148817"_s}, + {u"cost"_s, 1}, + {u"id"_s, u"31a2344e-0fce-4229-9453-fb2e8b6dd02c"_s}, + {u"is_user_input_required"_s, true}, + {u"title"_s, u"my reward"_s}, + {u"image"_s, defaultImage}, + }}}, + }}); + chan->addKnownChannelPointReward({{ + {u"channel_id"_s, u"11148817"_s}, + {u"id"_s, u"unused"_s}, + {u"reward"_s, + {{ + {u"channel_id"_s, u"11148817"_s}, + {u"cost"_s, 1}, + {u"id"_s, u"dc8d1dac-256e-42b9-b7ba-40b32e5294e2"_s}, + {u"is_user_input_required"_s, false}, + {u"title"_s, u"test"_s}, + {u"image"_s, defaultImage}, + }}}, + }}); + chan->addKnownChannelPointReward({{ + {u"channel_id"_s, u"11148817"_s}, + {u"id"_s, u"unused"_s}, + {u"reward"_s, + {{ + {u"channel_id"_s, u"11148817"_s}, + {u"cost"_s, 1}, + {u"default_bits_cost"_s, 2}, + {u"bits_cost"_s, 0}, + {u"pricing_type"_s, u"BITS"_s}, + {u"reward_type"_s, u"CELEBRATION"_s}, + {u"is_user_input_required"_s, false}, + {u"title"_s, u"BitReward"_s}, + {u"image"_s, defaultImage}, + }}}, + {u"redemption_metadata"_s, + QJsonObject{ + {u"celebration_emote_metadata"_s, + QJsonObject{ + {u"emote"_s, + {{ + {u"id"_s, u"42"_s}, + {u"token"_s, u"MyBitsEmote"_s}, + }}}, + }}, + }}, + }}); + + chan->setUserColor("UserColor", {1, 2, 3, 4}); + chan->setUserColor("UserColor2", {5, 6, 7, 8}); + chan->addRecentChatter("UserChatter"); + chan->addRecentChatter("UserColor"); + + chan->setCheerEmoteSets({ + HelixCheermoteSet{QJsonDocument::fromJson(CHEERMOTE_JSON).object()}, + }); + + chan->setFfzChannelBadges({{u"123456"_s, {3, 4}}}); + + chan->addTwitchBadgeSets(HelixChannelBadges{ + QJsonDocument::fromJson(LOCAL_BADGE_JSON).object(), + }); + + if (snapshot.param("ffzCustomVipBadge").toBool()) + { + chan->setFfzCustomVipBadge(std::make_shared(Emote{ + .name = {}, + .images = {Url{"https://chatterino.com/ffz-vip1x.png"}}, + .tooltip = {"VIP"}, + .homePage = {}, + })); + } + if (snapshot.param("ffzCustomModBadge").toBool()) + { + chan->setFfzCustomModBadge(std::make_shared(Emote{ + .name = {}, + .images = {Url{"https://chatterino.com/ffz-mod1x.png"}}, + .tooltip = {"Moderator"}, + .homePage = {}, + })); + } + + return chan; +} + +QT_WARNING_POP + } // namespace TEST(MessageBuilder, CommaSeparatedListTagParsing) @@ -408,7 +742,7 @@ TEST_F(TestMessageBuilder, ParseTwitchEmotes) for (const auto &test : testCases) { - auto *privmsg = static_cast( + auto *privmsg = dynamic_cast( Communi::IrcPrivateMessage::fromData(test.input, nullptr)); QString originalMessage = privmsg->content(); @@ -423,73 +757,6 @@ TEST_F(TestMessageBuilder, ParseTwitchEmotes) } } -TEST_F(TestMessageBuilder, ParseMessage) -{ - MockChannel channel("pajlada"); - - struct TestCase { - QByteArray input; - }; - - std::vector testCases{ - { - // action /me message - R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=90ef1e46-8baa-4bf2-9c54-272f39d6fa11;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662206235860;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :ACTION Kappa)", - }, - { - R"(@badge-info=subscriber/17;badges=subscriber/12,no_audio/1;color=#EBA2C0;display-name=jammehcow;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=9c2dd916-5a6d-4c1f-9fe7-a081b62a9c6b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662201093248;turbo=0;user-id=82674227;user-type= :jammehcow!jammehcow@jammehcow.tmi.twitch.tv PRIVMSG #pajlada :Kappa)", - }, - { - R"(@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=1902:0-4;first-msg=0;flags=;id=9b1c3cb9-7817-47ea-add1-f9d4a9b4f846;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201095690;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Keepo)", - }, - { - R"(@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=25:0-4/1902:6-10/305954156:12-19;first-msg=0;flags=;id=7be87072-bf24-4fa3-b3df-0ea6fa5f1474;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201102276;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Kappa Keepo PogChamp)", - }, - { - R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4,6-10;first-msg=0;flags=;id=f7516287-e5d1-43ca-974e-fe0cff84400b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204375009;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa Kappa)", - }, - { - R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emotes=25:0-4,8-12;first-msg=0;flags=;id=44f85d39-b5fb-475d-8555-f4244f2f7e82;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204423418;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa 😂 Kappa)", - }, - { - // start out of range - R"(@emotes=84608:9-10 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", - }, - { - // one character emote - R"(@emotes=84608:0-0 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", - }, - { - // two character emote - R"(@emotes=84609:0-1 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", - }, - { - // end out of range - R"(@emotes=84608:0-15 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", - }, - { - // range bad (end character before start) - R"(@emotes=84608:15-2 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", - }, - }; - - for (const auto &test : testCases) - { - auto *privmsg = dynamic_cast( - Communi::IrcPrivateMessage::fromData(test.input, nullptr)); - EXPECT_NE(privmsg, nullptr); - - QString originalMessage = privmsg->content(); - - MessageBuilder builder(&channel, privmsg, MessageParseArgs{}); - - auto msg = builder.build(); - EXPECT_NE(msg.get(), nullptr); - - delete privmsg; - } -} - TEST_F(TestMessageBuilder, IgnoresReplace) { struct TestCase { @@ -627,3 +894,172 @@ TEST_F(TestMessageBuilder, IgnoresReplace) << "' and output '" << message << "'"; } } + +class TestMessageBuilderP : public ::testing::TestWithParam +{ +public: + void SetUp() override + { + auto param = TestMessageBuilderP::GetParam(); + this->snapshot = testlib::Snapshot::read(IRC_CATEGORY, param); + + this->mockApplication = + std::make_unique(QString::fromUtf8( + this->snapshot->mergedSettings(SETTINGS_DEFAULT))); + auto mocks = MockEmotes::global(); + this->mockApplication->seventvEmotes.setGlobalEmotes(mocks.seventv); + this->mockApplication->bttvEmotes.setEmotes(mocks.bttv); + this->mockApplication->ffzEmotes.setEmotes(mocks.ffz); + this->mockApplication->getAccounts()->twitch.getCurrent()->setEmotes( + mocks.twitchAccount); + this->mockApplication->getUserData()->setUserColor(u"117691339"_s, + u"#DAA521"_s); + + this->mockApplication->getAccounts() + ->twitch.getCurrent() + ->blockUserLocally(u"12345"_s); + + auto makeBadge = [](QStringView platform) { + return std::make_shared(Emote{ + .name = {}, + .images = {Url{u"https://chatterino.com/" % platform % + u".png"}}, + .tooltip = {platform % u" badge"}, + .homePage = {}, + .zeroWidth = false, + .id = {}, + .author = {}, + .baseName = {}, + }); + }; + + // Chatterino + this->mockApplication->chatterinoBadges.setBadge( + {u"123456"_s}, makeBadge(u"Chatterino")); + + // FFZ + this->mockApplication->ffzBadges.registerBadge( + 1, {.emote = makeBadge(u"FFZ1"), .color = {9, 10, 11, 12}}); + this->mockApplication->ffzBadges.registerBadge( + 2, {.emote = makeBadge(u"FFZ2"), .color = {13, 14, 15, 16}}); + this->mockApplication->ffzBadges.registerBadge( + 3, {.emote = makeBadge(u"FFZ2"), .color = {17, 18, 19, 20}}); + this->mockApplication->ffzBadges.registerBadge( + 4, {.emote = makeBadge(u"FFZ2"), .color = {21, 22, 23, 24}}); + this->mockApplication->getFfzBadges()->assignBadgeToUser({u"123456"_s}, + 1); + this->mockApplication->getFfzBadges()->assignBadgeToUser({u"123456"_s}, + 2); + + // 7TV + this->mockApplication->getSeventvBadges()->registerBadge({ + {u"id"_s, u"1"_s}, + {u"tooltip"_s, u"7TV badge"_s}, + { + u"host"_s, + {{ + {u"url"_s, u"//chatterino.com/7tv/"_s}, + {u"files"_s, + QJsonArray{ + {{ + {u"name"_s, u"1x"_s}, + {u"format"_s, u"WEBP"_s}, + {u"width"_s, 16}, + }}, + }}, + }}, + }, + }); + this->mockApplication->getSeventvBadges()->assignBadgeToUser( + u"1"_s, {u"123456"_s}); + + // Twitch + this->mockApplication->getTwitchBadges()->loadLocalBadges(); + + this->twitchdevChannel = std::make_shared("twitchdev"); + this->twitchdevChannel->setRoomId("141981764"); + + auto tdMocks = MockEmotes::twitchdev(); + this->twitchdevChannel->setSeventvEmotes(std::move(tdMocks.seventv)); + this->twitchdevChannel->setBttvEmotes(std::move(tdMocks.bttv)); + this->twitchdevChannel->setFfzEmotes(std::move(tdMocks.ffz)); + + this->mockApplication->twitch.mockChannels.emplace( + "twitchdev", this->twitchdevChannel); + } + + void TearDown() override + { + this->twitchdevChannel.reset(); + this->mockApplication.reset(); + this->snapshot.reset(); + } + + std::shared_ptr twitchdevChannel; + std::unique_ptr mockApplication; + std::unique_ptr snapshot; +}; + +/// This tests the process of parsing IRC messages and emitting `MessagePtr`s. +/// +/// Even though it's in the message builder category, this uses +/// `IrcMesssageHandler` to ensure the correct (or: "real") arguments to build +/// messages. +/// +/// Tests are contained in `tests/snapshots/MessageBuilder/IRC`. Fixtures +/// consist of an object with the keys `input`, `output`, `settings` (optional), +/// and `params` (optional). +/// +/// `UPDATE_SNAPSHOTS` (top) controls whether the `output` will be generated or +/// checked. +/// +/// `params` is an optional object with the following keys: +/// - `prevMessages`: An array of past messages (used for replies) +/// - `findAllUsernames`: A boolean controlling the equally named setting +/// (default: false) +TEST_P(TestMessageBuilderP, Run) +{ + auto channel = makeMockTwitchChannel(u"pajlada"_s, *snapshot); + + std::vector prevMessages; + + for (auto prevInput : snapshot->param("prevMessages").toArray()) + { + auto *ircMessage = Communi::IrcMessage::fromData( + prevInput.toString().toUtf8(), nullptr); + ASSERT_NE(ircMessage, nullptr); + auto builtMessages = IrcMessageHandler::parseMessageWithReply( + channel.get(), ircMessage, prevMessages); + for (const auto &builtMessage : builtMessages) + { + prevMessages.emplace_back(builtMessage); + } + delete ircMessage; + } + + auto *ircMessage = + Communi::IrcMessage::fromData(snapshot->inputUtf8(), nullptr); + ASSERT_NE(ircMessage, nullptr); + + auto builtMessages = IrcMessageHandler::parseMessageWithReply( + channel.get(), ircMessage, prevMessages); + + QJsonArray got; + for (const auto &msg : builtMessages) + { + got.append(msg->toJson()); + } + + delete ircMessage; + + ASSERT_TRUE(snapshot->run(got, UPDATE_SNAPSHOTS)); +} + +INSTANTIATE_TEST_SUITE_P( + IrcMessage, TestMessageBuilderP, + testing::ValuesIn(testlib::Snapshot::discover(IRC_CATEGORY))); + +TEST(TestMessageBuilderP, Integrity) +{ + ASSERT_FALSE(UPDATE_SNAPSHOTS); // make sure fixtures are actually tested +} diff --git a/tests/src/lib/Snapshot.cpp b/tests/src/lib/Snapshot.cpp new file mode 100644 index 000000000..fb4749102 --- /dev/null +++ b/tests/src/lib/Snapshot.cpp @@ -0,0 +1,238 @@ +#include "lib/Snapshot.hpp" + +#include "common/Literals.hpp" + +#include +#include +#include +#include +#include + +namespace { + +using namespace chatterino::literals; + +bool compareJson(const QJsonValue &expected, const QJsonValue &got, + const QString &context) +{ + if (expected == got) + { + return true; + } + if (expected.type() != got.type()) + { + qWarning() << context + << "- mismatching type - expected:" << expected.type() + << "got:" << got.type(); + return false; + } + switch (expected.type()) + { + case QJsonValue::Array: { + auto expArr = expected.toArray(); + auto gotArr = got.toArray(); + if (expArr.size() != gotArr.size()) + { + qWarning() << context << "- Mismatching array size - expected:" + << expArr.size() << "got:" << gotArr.size(); + return false; + } + for (QJsonArray::size_type i = 0; i < expArr.size(); i++) + { + if (!compareJson(expArr[i], gotArr[i], + context % '[' % QString::number(i) % ']')) + { + return false; + } + } + } + break; // unreachable + case QJsonValue::Object: { + auto expObj = expected.toObject(); + auto gotObj = got.toObject(); + if (expObj.size() != gotObj.size()) + { + qWarning() << context << "- Mismatching object size - expected:" + << expObj.size() << "got:" << gotObj.size(); + return false; + } + + for (auto it = expObj.constBegin(); it != expObj.constEnd(); it++) + { + if (!gotObj.contains(it.key())) + { + qWarning() << context << "- Object doesn't contain key" + << it.key(); + return false; + } + if (!compareJson(it.value(), gotObj[it.key()], + context % '.' % it.key())) + { + return false; + } + } + } + break; + case QJsonValue::Null: + case QJsonValue::Bool: + case QJsonValue::Double: + case QJsonValue::String: + case QJsonValue::Undefined: + break; + } + + qWarning() << context << "- expected:" << expected << "got:" << got; + return false; +} + +void mergeJson(QJsonObject &base, const QJsonObject &additional) +{ + for (auto it = additional.begin(); it != additional.end(); it++) + { + auto ref = base[it.key()]; + + if (ref.isArray()) + { + // there's no way of pushing to the array without detaching first + auto arr = ref.toArray(); + if (!it->isArray()) + { + throw std::runtime_error("Mismatched types"); + } + + // append all additional values + auto addArr = it->toArray(); + for (auto v : addArr) + { + arr.append(v); + } + ref = arr; + continue; + } + + if (ref.isObject()) + { + // same here, detach first and overwrite + auto obj = ref.toObject(); + if (!it->isObject()) + { + throw std::runtime_error("Mismatched types"); + } + mergeJson(obj, it->toObject()); + ref = obj; + continue; + } + + ref = it.value(); // overwrite for simple types/non-existent keys + } +} + +QDir baseDir(const QString &category) +{ + QDir snapshotDir(QStringLiteral(__FILE__)); + snapshotDir.cd("../../../snapshots/"); + snapshotDir.cd(category); + return snapshotDir; +} + +QString filePath(const QString &category, const QString &name) +{ + return baseDir(category).filePath(name); +} + +} // namespace + +namespace chatterino::testlib { + +std::unique_ptr Snapshot::read(QString category, QString name) +{ + if (!name.endsWith(u".json")) + { + name.append(u".json"); + } + + QFile file(filePath(category, name)); + if (!file.open(QFile::ReadOnly)) + { + throw std::runtime_error("Failed to open file"); + } + auto content = file.readAll(); + file.close(); + const auto doc = QJsonDocument::fromJson(content).object(); + + return std::unique_ptr( + new Snapshot(std::move(category), std::move(name), doc)); +} + +QStringList Snapshot::discover(const QString &category) +{ + auto files = + baseDir(category).entryList(QDir::NoDotAndDotDot | QDir::Files); + for (auto &file : files) + { + file.remove(".json"); + } + return files; +} + +bool Snapshot::run(const QJsonValue &got, bool updateSnapshots) const +{ + if (updateSnapshots) + { + this->write(got); + return true; + } + + return compareJson(this->output_, got, QStringLiteral("output")); +} + +Snapshot::Snapshot(QString category, QString name, const QJsonObject &root) + : category_(std::move(category)) + , name_(std::move(name)) + , input_(root["input"_L1]) + , params_(root["params"_L1].toObject()) + , settings_(root["settings"_L1].toObject()) + , output_(root["output"_L1]) +{ +} + +void Snapshot::write(const QJsonValue &got) const +{ + QFile file(filePath(this->category_, this->name_)); + if (!file.open(QFile::WriteOnly)) + { + throw std::runtime_error("Failed to open file"); + } + + QJsonObject obj{ + {"input"_L1, this->input_}, + {"output"_L1, got}, + }; + if (!this->params_.isEmpty()) + { + obj.insert("params"_L1, this->params_); + } + if (!this->settings_.isEmpty()) + { + obj.insert("settings"_L1, this->settings_); + } + + file.write(QJsonDocument{obj}.toJson()); + file.close(); +} + +QByteArray Snapshot::mergedSettings(const QByteArray &base) const +{ + auto baseDoc = QJsonDocument::fromJson(base); + if (!baseDoc.isObject()) + { + throw std::runtime_error("Invalid base settings"); + } + auto baseObj = baseDoc.object(); + mergeJson(baseObj, this->settings_); + + baseDoc.setObject(baseObj); + return baseDoc.toJson(QJsonDocument::Compact); +} + +} // namespace chatterino::testlib diff --git a/tests/src/lib/Snapshot.hpp b/tests/src/lib/Snapshot.hpp new file mode 100644 index 000000000..39f663eaf --- /dev/null +++ b/tests/src/lib/Snapshot.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace chatterino::testlib { + +/// @brief JSON based snapshot/approval tests +/// +/// Snapshot tests record the output of some computation based on some @a input. +/// Additionally, users can provide @a params. There isn't any rule on what goes +/// into @a input vs. @a params - a rule of thumb is to put everything that's +/// not directly an input to the target function into @a params (like settings). +/// Similarly, settings can be specified in "settings". These can be merged with +/// existing settings (the base) in mergedSettings(). +/// +/// Snapshots are stored in `tests/snapshots/{category}/{name}.json`. +/// `category` can consist of multiple directories (e.g. `foo/bar`). +/// +/// Note that when using CTest, added snapshots are only discovered when +/// reloading the tests. +/// +/// @par A minimal example +/// +/// ```cpp +/// #include "lib/Snapshot.hpp" +/// #include "Test.hpp" +/// +/// #include +/// #include +/// +/// namespace testlib = chatterino::testlib; +/// +/// constexpr bool UPDATE_SNAPSHOTS = false; +/// +/// class ExampleTest : public ::testing::TestWithParam {}; +/// +/// TEST_P(ExampleTest, Run) { +/// auto fixture = testlib::Snapshot::read("category", GetParam()); +/// auto output = functionToTest(fixture.input()); // or input{String,Utf8} +/// // if snapshots are supposed to be updated, this will write the output +/// ASSERT_TRUE(fixture.run(output, UPDATE_SNAPSHOTS)); +/// } +/// +/// INSTANTIATE_TEST_SUITE_P(ExampleInstance, ExampleTest, +/// testing::ValuesIn(testlib::Snapshot::discover("category"))); +/// +/// // verify that all snapshots are included +/// TEST(ExampleTest, Integrity) { +/// ASSERT_FALSE(UPDATE_SNAPSHOTS); // make sure fixtures are actually tested +/// } +/// ``` +class Snapshot +{ +public: + Snapshot(const Snapshot &) = delete; + Snapshot &operator=(const Snapshot &) = delete; + + Snapshot(Snapshot &&) = default; + Snapshot &operator=(Snapshot &&) = default; + ~Snapshot() = default; + + /// Read a snapshot + static std::unique_ptr read(QString category, QString name); + + /// Finds all tests in @a category + static QStringList discover(const QString &category); + + /// @brief Runs the snapshot test + /// + /// If @a updateSnapshots is `false`, this checks that @a got matches the + /// expected output (#output()). + /// If @a updateSnapshots is `true`, this sets @a got as the expected + /// output of this snapshot. + bool run(const QJsonValue &got, bool updateSnapshots) const; + + QString name() const + { + return this->name_; + } + + QString category() const + { + return this->category_; + } + + QJsonValue input() const + { + return this->input_; + } + + QString inputString() const + { + return this->input_.toString(); + } + + QByteArray inputUtf8() const + { + return this->input_.toString().toUtf8(); + } + + QJsonValue param(QLatin1String name) const + { + return this->params_[name]; + } + QJsonValue param(const char *name) const + { + return this->param(QLatin1String{name}); + } + + QByteArray mergedSettings(const QByteArray &base) const; + + QJsonValue output() const + { + return this->output_; + } + +private: + Snapshot(QString category, QString name, const QJsonObject &root); + + void write(const QJsonValue &got) const; + + QString category_; + QString name_; + QJsonValue input_; + QJsonObject params_; + QJsonObject settings_; + QJsonValue output_; +}; + +} // namespace chatterino::testlib From 85a7f4a6a91b5fede338f8810975fa63973856d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:06:11 +0000 Subject: [PATCH 09/40] chore(deps): bump cmake/sanitizers-cmake from `3f0542e` to `0573e2e` (#5634) --- cmake/sanitizers-cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/sanitizers-cmake b/cmake/sanitizers-cmake index 3f0542e4e..0573e2ea8 160000 --- a/cmake/sanitizers-cmake +++ b/cmake/sanitizers-cmake @@ -1 +1 @@ -Subproject commit 3f0542e4e034aab417c51b2b22c94f83355dee15 +Subproject commit 0573e2ea8651b9bb3083f193c41eb086497cc80a From c0a5a3e8058255376494408730c9948aae8f66af Mon Sep 17 00:00:00 2001 From: Maverick Date: Mon, 14 Oct 2024 16:42:52 +0200 Subject: [PATCH 10/40] feat: Print proxy URL information in `/debug-env` command (#5648) --- CHANGELOG.md | 1 + src/controllers/commands/builtin/chatterino/Debugging.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cef1eba4..b150919ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Minor: The emote popup now reloads when Twitch emotes are reloaded. (#5580) - Minor: Added `--login ` CLI argument to specify which account to start logged in as. (#5626) - Minor: Indicate when subscriptions and resubscriptions are for multiple months. (#5642) +- Minor: Proxy URL information is now included in the `/debug-env` command. (#5648) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426, #5612) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed restricted users usernames not being clickable. (#5405) diff --git a/src/controllers/commands/builtin/chatterino/Debugging.cpp b/src/controllers/commands/builtin/chatterino/Debugging.cpp index 2984aa9e9..7a33dac8f 100644 --- a/src/controllers/commands/builtin/chatterino/Debugging.cpp +++ b/src/controllers/commands/builtin/chatterino/Debugging.cpp @@ -79,6 +79,7 @@ QString listEnvironmentVariables(const CommandContext &ctx) QStringList debugMessages{ "recentMessagesApiUrl: " + env.recentMessagesApiUrl, "linkResolverUrl: " + env.linkResolverUrl, + "proxyUrl: " + env.proxyUrl.value_or("N/A"), "twitchServerHost: " + env.twitchServerHost, "twitchServerPort: " + QString::number(env.twitchServerPort), "twitchServerSecure: " + QString::number(env.twitchServerSecure), From 6d139af553594afe81a37792b881e69e9cfac81f Mon Sep 17 00:00:00 2001 From: fourtf Date: Mon, 14 Oct 2024 20:10:59 +0200 Subject: [PATCH 11/40] Add hint to enable beautifier extension in Qt Creator (#5650) --- BUILDING_ON_WINDOWS.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/BUILDING_ON_WINDOWS.md b/BUILDING_ON_WINDOWS.md index 2449a329d..7af9b9a9a 100644 --- a/BUILDING_ON_WINDOWS.md +++ b/BUILDING_ON_WINDOWS.md @@ -170,9 +170,10 @@ To automatically format your code, do the following: 1. Download [LLVM 16.0.6](https://github.com/llvm/llvm-project/releases/download/llvmorg-16.0.6/LLVM-16.0.6-win64.exe) 2. During the installation, make sure to add it to your path -3. In Qt Creator, Select `Tools` > `Options` > `Beautifier` -4. Under `General` select `Tool: ClangFormat` and enable `Automatic Formatting on File Save` -5. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None` +3. Enable Beautifier under `Extensions` on the left (check "Load on start" and restart) +4. In Qt Creator, Select `Tools` > `Options` > `Beautifier` +5. Under `General` select `Tool: ClangFormat` and enable `Automatic Formatting on File Save` +6. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None` ### Building on MSVC with AddressSanitizer From 800f6df2cf5304f93dbecd4970023ac11d610be2 Mon Sep 17 00:00:00 2001 From: nerix Date: Fri, 18 Oct 2024 13:03:36 +0200 Subject: [PATCH 12/40] refactor(message-builder): move static helper methods to functions (#5652) --- CHANGELOG.md | 1 + src/CMakeLists.txt | 2 + src/controllers/ignores/IgnoreController.cpp | 305 ++++++++++++ src/controllers/ignores/IgnoreController.hpp | 18 + src/messages/MessageBuilder.cpp | 459 +------------------ src/messages/MessageBuilder.hpp | 28 +- src/providers/twitch/TwitchIrc.cpp | 166 +++++++ src/providers/twitch/TwitchIrc.hpp | 67 +++ tests/CMakeLists.txt | 2 + tests/src/IgnoreController.cpp | 186 ++++++++ tests/src/MessageBuilder.cpp | 448 ------------------ tests/src/TwitchIrc.cpp | 336 ++++++++++++++ 12 files changed, 1090 insertions(+), 928 deletions(-) create mode 100644 src/providers/twitch/TwitchIrc.cpp create mode 100644 src/providers/twitch/TwitchIrc.hpp create mode 100644 tests/src/IgnoreController.cpp create mode 100644 tests/src/TwitchIrc.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index b150919ed..54544119b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,7 @@ - Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607) - Dev: `GIFTimer` is no longer initialized in tests. (#5608) - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) +- Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652) ## 2.5.1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5c71aff95..4c24ef572 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -409,6 +409,8 @@ set(SOURCE_FILES providers/twitch/TwitchEmotes.hpp providers/twitch/TwitchHelpers.cpp providers/twitch/TwitchHelpers.hpp + providers/twitch/TwitchIrc.cpp + providers/twitch/TwitchIrc.hpp providers/twitch/TwitchIrcServer.cpp providers/twitch/TwitchIrcServer.hpp providers/twitch/TwitchUser.cpp diff --git a/src/controllers/ignores/IgnoreController.cpp b/src/controllers/ignores/IgnoreController.cpp index 7922b16dd..f8e60b6b9 100644 --- a/src/controllers/ignores/IgnoreController.cpp +++ b/src/controllers/ignores/IgnoreController.cpp @@ -1,12 +1,134 @@ #include "controllers/ignores/IgnoreController.hpp" #include "Application.hpp" +#include "common/Literals.hpp" #include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/ignores/IgnorePhrase.hpp" #include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchIrc.hpp" #include "singletons/Settings.hpp" +namespace { + +using namespace chatterino::literals; + +/** + * Computes (only) the replacement of @a match in @a source. + * The parts before and after the match in @a source are ignored. + * + * Occurrences of \b{\\1}, \b{\\2}, ..., in @a replacement are replaced + * with the string captured by the corresponding capturing group. + * This function should only be used if the regex contains capturing groups. + * + * Since Qt doesn't provide a way of replacing a single match with some replacement + * while supporting both capturing groups and lookahead/-behind in the regex, + * this is included here. It's essentially the implementation of + * QString::replace(const QRegularExpression &, const QString &). + * @see https://github.com/qt/qtbase/blob/97bb0ecfe628b5bb78e798563212adf02129c6f6/src/corelib/text/qstring.cpp#L4594-L4703 + */ +QString makeRegexReplacement(QStringView source, + const QRegularExpression ®ex, + const QRegularExpressionMatch &match, + const QString &replacement) +{ + using SizeType = QString::size_type; + struct QStringCapture { + SizeType pos; + SizeType len; + int captureNumber; + }; + + qsizetype numCaptures = regex.captureCount(); + + // 1. build the backreferences list, holding where the backreferences + // are in the replacement string + QVarLengthArray backReferences; + + SizeType replacementLength = replacement.size(); + for (SizeType i = 0; i < replacementLength - 1; i++) + { + if (replacement[i] != u'\\') + { + continue; + } + + int no = replacement[i + 1].digitValue(); + if (no <= 0 || no > numCaptures) + { + continue; + } + + QStringCapture backReference{.pos = i, .len = 2}; + + if (i < replacementLength - 2) + { + int secondDigit = replacement[i + 2].digitValue(); + if (secondDigit != -1 && ((no * 10) + secondDigit) <= numCaptures) + { + no = (no * 10) + secondDigit; + ++backReference.len; + } + } + + backReference.captureNumber = no; + backReferences.append(backReference); + } + + // 2. iterate on the matches. + // For every match, copy the replacement string in chunks + // with the proper replacements for the backreferences + + // length of the new string, with all the replacements + SizeType newLength = 0; + QVarLengthArray chunks; + QStringView replacementView{replacement}; + + // Initially: empty, as we only care about the replacement + SizeType len = 0; + SizeType lastEnd = 0; + for (const QStringCapture &backReference : std::as_const(backReferences)) + { + // part of "replacement" before the backreference + len = backReference.pos - lastEnd; + if (len > 0) + { + chunks << replacementView.mid(lastEnd, len); + newLength += len; + } + + // backreference itself + len = match.capturedLength(backReference.captureNumber); + if (len > 0) + { + chunks << source.mid( + match.capturedStart(backReference.captureNumber), len); + newLength += len; + } + + lastEnd = backReference.pos + backReference.len; + } + + // add the last part of the replacement string + len = replacementView.size() - lastEnd; + if (len > 0) + { + chunks << replacementView.mid(lastEnd, len); + newLength += len; + } + + // 3. assemble the chunks together + QString dst; + dst.reserve(newLength); + for (const QStringView &chunk : std::as_const(chunks)) + { + dst += chunk; + } + return dst; +} + +} // namespace + namespace chatterino { bool isIgnoredMessage(IgnoredMessageParameters &¶ms) @@ -65,4 +187,187 @@ bool isIgnoredMessage(IgnoredMessageParameters &¶ms) return false; } +void processIgnorePhrases(const std::vector &phrases, + QString &content, + std::vector &twitchEmotes) +{ + using SizeType = QString::size_type; + + auto removeEmotesInRange = [&twitchEmotes](SizeType pos, SizeType len) { + // all emotes outside the range come before `it` + // all emotes in the range start at `it` + auto it = std::partition( + twitchEmotes.begin(), twitchEmotes.end(), + [pos, len](const auto &item) { + // returns true for emotes outside the range + return !((item.start >= pos) && item.start < (pos + len)); + }); + std::vector emotesInRange(it, + twitchEmotes.end()); + twitchEmotes.erase(it, twitchEmotes.end()); + return emotesInRange; + }; + + auto shiftIndicesAfter = [&twitchEmotes](int pos, int by) { + for (auto &item : twitchEmotes) + { + auto &index = item.start; + if (index >= pos) + { + index += by; + item.end += by; + } + } + }; + + auto addReplEmotes = [&twitchEmotes](const IgnorePhrase &phrase, + const auto &midrepl, + SizeType startIndex) { + if (!phrase.containsEmote()) + { + return; + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + auto words = midrepl.tokenize(u' '); +#else + auto words = midrepl.split(' '); +#endif + SizeType pos = 0; + for (const auto &word : words) + { + for (const auto &emote : phrase.getEmotes()) + { + if (word == emote.first.string) + { + if (emote.second == nullptr) + { + qCDebug(chatterinoTwitch) + << "emote null" << emote.first.string; + } + twitchEmotes.push_back(TwitchEmoteOccurrence{ + static_cast(startIndex + pos), + static_cast(startIndex + pos + + emote.first.string.length()), + emote.second, + emote.first, + }); + } + } + pos += word.length() + 1; + } + }; + + auto replaceMessageAt = [&](const IgnorePhrase &phrase, SizeType from, + SizeType length, const QString &replacement) { + auto removedEmotes = removeEmotesInRange(from, length); + content.replace(from, length, replacement); + auto wordStart = from; + while (wordStart > 0) + { + if (content[wordStart - 1] == ' ') + { + break; + } + --wordStart; + } + auto wordEnd = from + replacement.length(); + while (wordEnd < content.length()) + { + if (content[wordEnd] == ' ') + { + break; + } + ++wordEnd; + } + + shiftIndicesAfter(static_cast(from + length), + static_cast(replacement.length() - length)); + + auto midExtendedRef = + QStringView{content}.mid(wordStart, wordEnd - wordStart); + + for (auto &emote : removedEmotes) + { + if (emote.ptr == nullptr) + { + qCDebug(chatterinoTwitch) + << "Invalid emote occurrence" << emote.name.string; + continue; + } + QRegularExpression emoteregex( + "\\b" + emote.name.string + "\\b", + QRegularExpression::UseUnicodePropertiesOption); +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + auto match = emoteregex.matchView(midExtendedRef); +#else + auto match = emoteregex.match(midExtendedRef); +#endif + if (match.hasMatch()) + { + emote.start = static_cast(from + match.capturedStart()); + emote.end = static_cast(from + match.capturedEnd()); + twitchEmotes.push_back(std::move(emote)); + } + } + + addReplEmotes(phrase, midExtendedRef, wordStart); + }; + + for (const auto &phrase : phrases) + { + if (phrase.isBlock()) + { + continue; + } + const auto &pattern = phrase.getPattern(); + if (pattern.isEmpty()) + { + continue; + } + if (phrase.isRegex()) + { + const auto ®ex = phrase.getRegex(); + if (!regex.isValid()) + { + continue; + } + + QRegularExpressionMatch match; + size_t iterations = 0; + SizeType from = 0; + while ((from = content.indexOf(regex, from, &match)) != -1) + { + auto replacement = phrase.getReplace(); + if (regex.captureCount() > 0) + { + replacement = makeRegexReplacement(content, regex, match, + replacement); + } + + replaceMessageAt(phrase, from, match.capturedLength(), + replacement); + from += phrase.getReplace().length(); + iterations++; + if (iterations >= 128) + { + content = u"Too many replacements - check your ignores!"_s; + return; + } + } + + continue; + } + + SizeType from = 0; + while ((from = content.indexOf(pattern, from, + phrase.caseSensitivity())) != -1) + { + replaceMessageAt(phrase, from, pattern.length(), + phrase.getReplace()); + from += phrase.getReplace().length(); + } + } +} + } // namespace chatterino diff --git a/src/controllers/ignores/IgnoreController.hpp b/src/controllers/ignores/IgnoreController.hpp index 4c2048621..955531537 100644 --- a/src/controllers/ignores/IgnoreController.hpp +++ b/src/controllers/ignores/IgnoreController.hpp @@ -2,8 +2,13 @@ #include +#include + namespace chatterino { +class IgnorePhrase; +struct TwitchEmoteOccurrence; + enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster }; struct IgnoredMessageParameters { @@ -16,4 +21,17 @@ struct IgnoredMessageParameters { bool isIgnoredMessage(IgnoredMessageParameters &¶ms); +/// @brief Processes replacement ignore-phrases for a message +/// +/// @param phrases A list of IgnorePhrases to process. Block phrases as well as +/// invalid phrases are ignored. +/// @param content The message text. This gets altered by replacements. +/// @param twitchEmotes A list of emotes present in the message. Occurrences +/// that have been removed from the message will also be +/// removed in this list. Similarly, if new emotes are added +/// from a replacement, this list gets updated as well. +void processIgnorePhrases(const std::vector &phrases, + QString &content, + std::vector &twitchEmotes); + } // namespace chatterino diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 3802497b0..e470b01ce 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -30,6 +30,7 @@ #include "providers/twitch/TwitchBadge.hpp" #include "providers/twitch/TwitchBadges.hpp" #include "providers/twitch/TwitchChannel.hpp" +#include "providers/twitch/TwitchIrc.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Emotes.hpp" #include "singletons/Resources.hpp" @@ -237,77 +238,6 @@ QString stylizeUsername(const QString &username, const Message &message) return usernameText; } -void appendTwitchEmoteOccurrences(const QString &emote, - std::vector &vec, - const std::vector &correctPositions, - const QString &originalMessage, - int messageOffset) -{ - auto *app = getApp(); - if (!emote.contains(':')) - { - return; - } - - auto parameters = emote.split(':'); - - if (parameters.length() < 2) - { - return; - } - - auto id = EmoteId{parameters.at(0)}; - - auto occurrences = parameters.at(1).split(','); - - for (const QString &occurrence : occurrences) - { - auto coords = occurrence.split('-'); - - if (coords.length() < 2) - { - return; - } - - auto from = coords.at(0).toUInt() - messageOffset; - auto to = coords.at(1).toUInt() - messageOffset; - auto maxPositions = correctPositions.size(); - if (from > to || to >= maxPositions) - { - // Emote coords are out of range - qCDebug(chatterinoTwitch) - << "Emote coords" << from << "-" << to << "are out of range (" - << maxPositions << ")"; - return; - } - - auto start = correctPositions[from]; - auto end = correctPositions[to]; - if (start > end || start < 0 || end > originalMessage.length()) - { - // Emote coords are out of range from the modified character positions - qCDebug(chatterinoTwitch) << "Emote coords" << from << "-" << to - << "are out of range after offsets (" - << originalMessage.length() << ")"; - return; - } - - auto name = EmoteName{originalMessage.mid(start, end - start + 1)}; - TwitchEmoteOccurrence emoteOccurrence{ - start, - end, - app->getEmotes()->getTwitchEmotes()->getOrCreateEmote(id, name), - name, - }; - if (emoteOccurrence.ptr == nullptr) - { - qCDebug(chatterinoTwitch) - << "nullptr" << emoteOccurrence.name.string; - } - vec.push_back(std::move(emoteOccurrence)); - } -} - std::optional getTwitchBadge(const Badge &badge, const TwitchChannel *twitchChannel) { @@ -420,120 +350,6 @@ void appendBadges(MessageBuilder *builder, const std::vector &badges, builder->message().badgeInfos = badgeInfos; } -/** - * Computes (only) the replacement of @a match in @a source. - * The parts before and after the match in @a source are ignored. - * - * Occurrences of \b{\\1}, \b{\\2}, ..., in @a replacement are replaced - * with the string captured by the corresponding capturing group. - * This function should only be used if the regex contains capturing groups. - * - * Since Qt doesn't provide a way of replacing a single match with some replacement - * while supporting both capturing groups and lookahead/-behind in the regex, - * this is included here. It's essentially the implementation of - * QString::replace(const QRegularExpression &, const QString &). - * @see https://github.com/qt/qtbase/blob/97bb0ecfe628b5bb78e798563212adf02129c6f6/src/corelib/text/qstring.cpp#L4594-L4703 - */ -QString makeRegexReplacement(QStringView source, - const QRegularExpression ®ex, - const QRegularExpressionMatch &match, - const QString &replacement) -{ - using SizeType = QString::size_type; - struct QStringCapture { - SizeType pos; - SizeType len; - int captureNumber; - }; - - qsizetype numCaptures = regex.captureCount(); - - // 1. build the backreferences list, holding where the backreferences - // are in the replacement string - QVarLengthArray backReferences; - - SizeType replacementLength = replacement.size(); - for (SizeType i = 0; i < replacementLength - 1; i++) - { - if (replacement[i] != u'\\') - { - continue; - } - - int no = replacement[i + 1].digitValue(); - if (no <= 0 || no > numCaptures) - { - continue; - } - - QStringCapture backReference{.pos = i, .len = 2}; - - if (i < replacementLength - 2) - { - int secondDigit = replacement[i + 2].digitValue(); - if (secondDigit != -1 && ((no * 10) + secondDigit) <= numCaptures) - { - no = (no * 10) + secondDigit; - ++backReference.len; - } - } - - backReference.captureNumber = no; - backReferences.append(backReference); - } - - // 2. iterate on the matches. - // For every match, copy the replacement string in chunks - // with the proper replacements for the backreferences - - // length of the new string, with all the replacements - SizeType newLength = 0; - QVarLengthArray chunks; - QStringView replacementView{replacement}; - - // Initially: empty, as we only care about the replacement - SizeType len = 0; - SizeType lastEnd = 0; - for (const QStringCapture &backReference : std::as_const(backReferences)) - { - // part of "replacement" before the backreference - len = backReference.pos - lastEnd; - if (len > 0) - { - chunks << replacementView.mid(lastEnd, len); - newLength += len; - } - - // backreference itself - len = match.capturedLength(backReference.captureNumber); - if (len > 0) - { - chunks << source.mid( - match.capturedStart(backReference.captureNumber), len); - newLength += len; - } - - lastEnd = backReference.pos + backReference.len; - } - - // add the last part of the replacement string - len = replacementView.size() - lastEnd; - if (len > 0) - { - chunks << replacementView.mid(lastEnd, len); - newLength += len; - } - - // 3. assemble the chunks together - QString dst; - dst.reserve(newLength); - for (const QStringView &chunk : std::as_const(chunks)) - { - dst += chunk; - } - return dst; -} - bool doesWordContainATwitchEmote( int cursor, const QString &word, const std::vector &twitchEmotes, @@ -1358,13 +1174,12 @@ MessagePtr MessageBuilder::build() } // Twitch emotes - auto twitchEmotes = MessageBuilder::parseTwitchEmotes( - this->tags, this->originalMessage_, this->messageOffset_); + auto twitchEmotes = parseTwitchEmotes(this->tags, this->originalMessage_, + this->messageOffset_); // This runs through all ignored phrases and runs its replacements on this->originalMessage_ - MessageBuilder::processIgnorePhrases( - *getSettings()->ignoredMessages.readOnly(), this->originalMessage_, - twitchEmotes); + processIgnorePhrases(*getSettings()->ignoredMessages.readOnly(), + this->originalMessage_, twitchEmotes); std::sort(twitchEmotes.begin(), twitchEmotes.end(), [](const auto &a, const auto &b) { @@ -2178,268 +1993,6 @@ MessagePtr MessageBuilder::makeLowTrustUpdateMessage( return builder.release(); } -std::unordered_map MessageBuilder::parseBadgeInfoTag( - const QVariantMap &tags) -{ - std::unordered_map infoMap; - - auto infoIt = tags.constFind("badge-info"); - if (infoIt == tags.end()) - { - return infoMap; - } - - auto info = infoIt.value().toString().split(',', Qt::SkipEmptyParts); - - for (const QString &badge : info) - { - infoMap.emplace(slashKeyValue(badge)); - } - - return infoMap; -} - -std::vector MessageBuilder::parseBadgeTag(const QVariantMap &tags) -{ - std::vector b; - - auto badgesIt = tags.constFind("badges"); - if (badgesIt == tags.end()) - { - return b; - } - - auto badges = badgesIt.value().toString().split(',', Qt::SkipEmptyParts); - - for (const QString &badge : badges) - { - if (!badge.contains('/')) - { - continue; - } - - auto pair = slashKeyValue(badge); - b.emplace_back(Badge{pair.first, pair.second}); - } - - return b; -} - -std::vector MessageBuilder::parseTwitchEmotes( - const QVariantMap &tags, const QString &originalMessage, int messageOffset) -{ - // Twitch emotes - std::vector twitchEmotes; - - auto emotesTag = tags.find("emotes"); - - if (emotesTag == tags.end()) - { - return twitchEmotes; - } - - QStringList emoteString = emotesTag.value().toString().split('/'); - std::vector correctPositions; - for (int i = 0; i < originalMessage.size(); ++i) - { - if (!originalMessage.at(i).isLowSurrogate()) - { - correctPositions.push_back(i); - } - } - for (const QString &emote : emoteString) - { - appendTwitchEmoteOccurrences(emote, twitchEmotes, correctPositions, - originalMessage, messageOffset); - } - - return twitchEmotes; -} - -void MessageBuilder::processIgnorePhrases( - const std::vector &phrases, QString &originalMessage, - std::vector &twitchEmotes) -{ - using SizeType = QString::size_type; - - auto removeEmotesInRange = [&twitchEmotes](SizeType pos, SizeType len) { - // all emotes outside the range come before `it` - // all emotes in the range start at `it` - auto it = std::partition( - twitchEmotes.begin(), twitchEmotes.end(), - [pos, len](const auto &item) { - // returns true for emotes outside the range - return !((item.start >= pos) && item.start < (pos + len)); - }); - std::vector emotesInRange(it, - twitchEmotes.end()); - twitchEmotes.erase(it, twitchEmotes.end()); - return emotesInRange; - }; - - auto shiftIndicesAfter = [&twitchEmotes](int pos, int by) { - for (auto &item : twitchEmotes) - { - auto &index = item.start; - if (index >= pos) - { - index += by; - item.end += by; - } - } - }; - - auto addReplEmotes = [&twitchEmotes](const IgnorePhrase &phrase, - const auto &midrepl, - SizeType startIndex) { - if (!phrase.containsEmote()) - { - return; - } - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - auto words = midrepl.tokenize(u' '); -#else - auto words = midrepl.split(' '); -#endif - SizeType pos = 0; - for (const auto &word : words) - { - for (const auto &emote : phrase.getEmotes()) - { - if (word == emote.first.string) - { - if (emote.second == nullptr) - { - qCDebug(chatterinoTwitch) - << "emote null" << emote.first.string; - } - twitchEmotes.push_back(TwitchEmoteOccurrence{ - static_cast(startIndex + pos), - static_cast(startIndex + pos + - emote.first.string.length()), - emote.second, - emote.first, - }); - } - } - pos += word.length() + 1; - } - }; - - auto replaceMessageAt = [&](const IgnorePhrase &phrase, SizeType from, - SizeType length, const QString &replacement) { - auto removedEmotes = removeEmotesInRange(from, length); - originalMessage.replace(from, length, replacement); - auto wordStart = from; - while (wordStart > 0) - { - if (originalMessage[wordStart - 1] == ' ') - { - break; - } - --wordStart; - } - auto wordEnd = from + replacement.length(); - while (wordEnd < originalMessage.length()) - { - if (originalMessage[wordEnd] == ' ') - { - break; - } - ++wordEnd; - } - - shiftIndicesAfter(static_cast(from + length), - static_cast(replacement.length() - length)); - - auto midExtendedRef = - QStringView{originalMessage}.mid(wordStart, wordEnd - wordStart); - - for (auto &emote : removedEmotes) - { - if (emote.ptr == nullptr) - { - qCDebug(chatterinoTwitch) - << "Invalid emote occurrence" << emote.name.string; - continue; - } - QRegularExpression emoteregex( - "\\b" + emote.name.string + "\\b", - QRegularExpression::UseUnicodePropertiesOption); -#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) - auto match = emoteregex.matchView(midExtendedRef); -#else - auto match = emoteregex.match(midExtendedRef); -#endif - if (match.hasMatch()) - { - emote.start = static_cast(from + match.capturedStart()); - emote.end = static_cast(from + match.capturedEnd()); - twitchEmotes.push_back(std::move(emote)); - } - } - - addReplEmotes(phrase, midExtendedRef, wordStart); - }; - - for (const auto &phrase : phrases) - { - if (phrase.isBlock()) - { - continue; - } - const auto &pattern = phrase.getPattern(); - if (pattern.isEmpty()) - { - continue; - } - if (phrase.isRegex()) - { - const auto ®ex = phrase.getRegex(); - if (!regex.isValid()) - { - continue; - } - - QRegularExpressionMatch match; - size_t iterations = 0; - SizeType from = 0; - while ((from = originalMessage.indexOf(regex, from, &match)) != -1) - { - auto replacement = phrase.getReplace(); - if (regex.captureCount() > 0) - { - replacement = makeRegexReplacement(originalMessage, regex, - match, replacement); - } - - replaceMessageAt(phrase, from, match.capturedLength(), - replacement); - from += phrase.getReplace().length(); - iterations++; - if (iterations >= 128) - { - originalMessage = - u"Too many replacements - check your ignores!"_s; - return; - } - } - - continue; - } - - SizeType from = 0; - while ((from = originalMessage.indexOf(pattern, from, - phrase.caseSensitivity())) != -1) - { - replaceMessageAt(phrase, from, pattern.length(), - phrase.getReplace()); - from += phrase.getReplace().length(); - } - } -} - void MessageBuilder::addTextOrEmoji(EmotePtr emote) { this->emplace(emote, MessageElementFlag::EmojiAll); @@ -3159,7 +2712,7 @@ void MessageBuilder::appendTwitchBadges() return; } - auto badgeInfos = MessageBuilder::parseBadgeInfoTag(this->tags); + auto badgeInfos = parseBadgeInfoTag(this->tags); auto badges = parseBadgeTag(this->tags); appendBadges(this, badges, badgeInfos, this->twitchChannel); } diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 651de4045..aa2933b64 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -45,6 +45,7 @@ struct HelixVip; using HelixModerator = HelixVip; struct ChannelPointReward; struct DeleteAction; +struct TwitchEmoteOccurrence; namespace linkparser { struct Parsed; @@ -89,19 +90,6 @@ struct MessageParseArgs { QString channelPointRewardId = ""; }; -struct TwitchEmoteOccurrence { - int start; - int end; - EmotePtr ptr; - EmoteName name; - - bool operator==(const TwitchEmoteOccurrence &other) const - { - return std::tie(this->start, this->end, this->ptr, this->name) == - std::tie(other.start, other.end, other.ptr, other.name); - } -}; - class MessageBuilder { public: @@ -237,20 +225,6 @@ public: static MessagePtr makeLowTrustUpdateMessage( const PubSubLowTrustUsersMessage &action); - static std::unordered_map parseBadgeInfoTag( - const QVariantMap &tags); - - // Parses "badges" tag which contains a comma separated list of key-value elements - static std::vector parseBadgeTag(const QVariantMap &tags); - - static std::vector parseTwitchEmotes( - const QVariantMap &tags, const QString &originalMessage, - int messageOffset); - - static void processIgnorePhrases( - const std::vector &phrases, QString &originalMessage, - std::vector &twitchEmotes); - protected: void addTextOrEmoji(EmotePtr emote); void addTextOrEmoji(const QString &string_); diff --git a/src/providers/twitch/TwitchIrc.cpp b/src/providers/twitch/TwitchIrc.cpp new file mode 100644 index 000000000..962618f6b --- /dev/null +++ b/src/providers/twitch/TwitchIrc.cpp @@ -0,0 +1,166 @@ +#include "providers/twitch/TwitchIrc.hpp" + +#include "Application.hpp" +#include "common/Aliases.hpp" +#include "common/QLogging.hpp" +#include "singletons/Emotes.hpp" +#include "util/IrcHelpers.hpp" + +namespace { + +using namespace chatterino; + +void appendTwitchEmoteOccurrences(const QString &emote, + std::vector &vec, + const std::vector &correctPositions, + const QString &originalMessage, + int messageOffset) +{ + auto *app = getApp(); + if (!emote.contains(':')) + { + return; + } + + auto parameters = emote.split(':'); + + if (parameters.length() < 2) + { + return; + } + + auto id = EmoteId{parameters.at(0)}; + + auto occurrences = parameters.at(1).split(','); + + for (const QString &occurrence : occurrences) + { + auto coords = occurrence.split('-'); + + if (coords.length() < 2) + { + return; + } + + auto from = coords.at(0).toUInt() - messageOffset; + auto to = coords.at(1).toUInt() - messageOffset; + auto maxPositions = correctPositions.size(); + if (from > to || to >= maxPositions) + { + // Emote coords are out of range + qCDebug(chatterinoTwitch) + << "Emote coords" << from << "-" << to << "are out of range (" + << maxPositions << ")"; + return; + } + + auto start = correctPositions[from]; + auto end = correctPositions[to]; + if (start > end || start < 0 || end > originalMessage.length()) + { + // Emote coords are out of range from the modified character positions + qCDebug(chatterinoTwitch) << "Emote coords" << from << "-" << to + << "are out of range after offsets (" + << originalMessage.length() << ")"; + return; + } + + auto name = EmoteName{originalMessage.mid(start, end - start + 1)}; + TwitchEmoteOccurrence emoteOccurrence{ + start, + end, + app->getEmotes()->getTwitchEmotes()->getOrCreateEmote(id, name), + name, + }; + if (emoteOccurrence.ptr == nullptr) + { + qCDebug(chatterinoTwitch) + << "nullptr" << emoteOccurrence.name.string; + } + vec.push_back(std::move(emoteOccurrence)); + } +} + +} // namespace + +namespace chatterino { + +std::unordered_map parseBadgeInfoTag(const QVariantMap &tags) +{ + std::unordered_map infoMap; + + auto infoIt = tags.constFind("badge-info"); + if (infoIt == tags.end()) + { + return infoMap; + } + + auto info = infoIt.value().toString().split(',', Qt::SkipEmptyParts); + + for (const QString &badge : info) + { + infoMap.emplace(slashKeyValue(badge)); + } + + return infoMap; +} + +std::vector parseBadgeTag(const QVariantMap &tags) +{ + std::vector b; + + auto badgesIt = tags.constFind("badges"); + if (badgesIt == tags.end()) + { + return b; + } + + auto badges = badgesIt.value().toString().split(',', Qt::SkipEmptyParts); + + for (const QString &badge : badges) + { + if (!badge.contains('/')) + { + continue; + } + + auto pair = slashKeyValue(badge); + b.emplace_back(Badge{pair.first, pair.second}); + } + + return b; +} + +std::vector parseTwitchEmotes(const QVariantMap &tags, + const QString &content, + int messageOffset) +{ + // Twitch emotes + std::vector twitchEmotes; + + auto emotesTag = tags.find("emotes"); + + if (emotesTag == tags.end()) + { + return twitchEmotes; + } + + QStringList emoteString = emotesTag.value().toString().split('/'); + std::vector correctPositions; + for (int i = 0; i < content.size(); ++i) + { + if (!content.at(i).isLowSurrogate()) + { + correctPositions.push_back(i); + } + } + for (const QString &emote : emoteString) + { + appendTwitchEmoteOccurrences(emote, twitchEmotes, correctPositions, + content, messageOffset); + } + + return twitchEmotes; +} + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchIrc.hpp b/src/providers/twitch/TwitchIrc.hpp new file mode 100644 index 000000000..60529fb77 --- /dev/null +++ b/src/providers/twitch/TwitchIrc.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "messages/Emote.hpp" +#include "providers/twitch/TwitchBadge.hpp" + +#include +#include + +#include + +namespace chatterino { + +struct TwitchEmoteOccurrence { + int start; + int end; + EmotePtr ptr; + EmoteName name; + + bool operator==(const TwitchEmoteOccurrence &other) const + { + return std::tie(this->start, this->end, this->ptr, this->name) == + std::tie(other.start, other.end, other.ptr, other.name); + } +}; + +/// @brief Parses the `badge-info` tag of an IRC message +/// +/// The `badge-info` tag maps badge-names to a value. Subscriber badges, for +/// example, are mapped to the number of months the chatter is subscribed for. +/// +/// **Example**: +/// `badge-info=subscriber/22` would be parsed as `{ subscriber => 22 }` +/// +/// @param tags The tags of the IRC message +/// @returns A map of badge-names to their values +std::unordered_map parseBadgeInfoTag(const QVariantMap &tags); + +/// @brief Parses the `badges` tag of an IRC message +/// +/// The `badges` tag contains a comma separated list of key-value elements which +/// make up the name and version of each badge. +/// +/// **Example**: +/// `badges=broadcaster/1,subscriber/18` would be parsed as +/// `[(broadcaster, 1), (subscriber, 18)]` +/// +/// @param tags The tags of the IRC message +/// @returns A list of badges (name and version) +std::vector parseBadgeTag(const QVariantMap &tags); + +/// @brief Parses Twitch emotes in an IRC message +/// +/// @param tags The tags of the IRC message +/// @param content The message text. This might be shortened due to skipping +/// content at the start. `messageOffset` describes this offset. +/// @param messageOffset The offset of `content` compared to the original +/// message text. Used for calculating indices into the +/// message. An offset of 3, for example, indicates that +/// `content` excludes the first three characters of the +/// original message (`@a foo` (original message) -> `foo` +/// (content)). +/// @returns A list of emotes and their positions +std::vector parseTwitchEmotes(const QVariantMap &tags, + const QString &content, + int messageOffset); + +} // namespace chatterino diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 36e7e31d8..8a6647fee 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,6 +48,8 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp ${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.cpp ${CMAKE_CURRENT_LIST_DIR}/src/CancellationToken.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/TwitchIrc.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/IgnoreController.cpp ${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp ${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.hpp # Add your new file above this line! diff --git a/tests/src/IgnoreController.cpp b/tests/src/IgnoreController.cpp new file mode 100644 index 000000000..f3e061f51 --- /dev/null +++ b/tests/src/IgnoreController.cpp @@ -0,0 +1,186 @@ +#include "controllers/ignores/IgnoreController.hpp" + +#include "controllers/accounts/AccountController.hpp" +#include "mocks/BaseApplication.hpp" +#include "mocks/Emotes.hpp" +#include "providers/twitch/TwitchIrc.hpp" +#include "Test.hpp" + +using namespace chatterino; + +namespace { + +class MockApplication : public mock::BaseApplication +{ +public: + MockApplication() = default; + + IEmotes *getEmotes() override + { + return &this->emotes; + } + + AccountController *getAccounts() override + { + return &this->accounts; + } + + mock::Emotes emotes; + AccountController accounts; +}; + +} // namespace + +class TestIgnoreController : public ::testing::Test +{ +protected: + void SetUp() override + { + this->mockApplication = std::make_unique(); + } + + void TearDown() override + { + this->mockApplication.reset(); + } + + std::unique_ptr mockApplication; +}; + +TEST_F(TestIgnoreController, processIgnorePhrases) +{ + struct TestCase { + std::vector phrases; + QString input; + std::vector twitchEmotes; + QString expectedMessage; + std::vector expectedTwitchEmotes; + }; + + auto *twitchEmotes = this->mockApplication->getEmotes()->getTwitchEmotes(); + + auto emoteAt = [&](int at, const QString &name) { + return TwitchEmoteOccurrence{ + .start = at, + .end = static_cast(at + name.size() - 1), + .ptr = + twitchEmotes->getOrCreateEmote(EmoteId{name}, EmoteName{name}), + .name = EmoteName{name}, + }; + }; + + auto regularReplace = [](auto pattern, auto replace, + bool caseSensitive = true) { + return IgnorePhrase(pattern, false, false, replace, caseSensitive); + }; + auto regexReplace = [](auto pattern, auto regex, + bool caseSensitive = true) { + return IgnorePhrase(pattern, true, false, regex, caseSensitive); + }; + + std::vector testCases{ + { + {regularReplace("foo1", "baz1")}, + "foo1 Kappa", + {emoteAt(4, "Kappa")}, + "baz1 Kappa", + {emoteAt(4, "Kappa")}, + }, + { + {regularReplace("foo1", "baz1", false)}, + "FoO1 Kappa", + {emoteAt(4, "Kappa")}, + "baz1 Kappa", + {emoteAt(4, "Kappa")}, + }, + { + {regexReplace("f(o+)1", "baz1[\\1]")}, + "foo1 Kappa", + {emoteAt(4, "Kappa")}, + "baz1[oo] Kappa", + {emoteAt(8, "Kappa")}, + }, + + { + {regexReplace("f(o+)1", R"(baz1[\0][\1][\2])")}, + "foo1 Kappa", + {emoteAt(4, "Kappa")}, + "baz1[\\0][oo][\\2] Kappa", + {emoteAt(16, "Kappa")}, + }, + { + {regexReplace("f(o+)(\\d+)", "baz1[\\1+\\2]")}, + "foo123 Kappa", + {emoteAt(6, "Kappa")}, + "baz1[oo+123] Kappa", + {emoteAt(12, "Kappa")}, + }, + { + {regexReplace("(?<=foo)(\\d+)", "[\\1]")}, + "foo123 Kappa", + {emoteAt(6, "Kappa")}, + "foo[123] Kappa", + {emoteAt(8, "Kappa")}, + }, + { + {regexReplace("a(?=a| )", "b")}, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " + "Kappa", + {emoteAt(127, "Kappa")}, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "bbbb" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb " + "Kappa", + {emoteAt(127, "Kappa")}, + }, + { + {regexReplace("abc", "def", false)}, + "AbC Kappa", + {emoteAt(3, "Kappa")}, + "def Kappa", + {emoteAt(3, "Kappa")}, + }, + { + { + regexReplace("abc", "def", false), + regularReplace("def", "ghi"), + }, + "AbC Kappa", + {emoteAt(3, "Kappa")}, + "ghi Kappa", + {emoteAt(3, "Kappa")}, + }, + { + { + regexReplace("a(?=a| )", "b"), + regexReplace("b(?=b| )", "c"), + }, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " + "Kappa", + {emoteAt(127, "Kappa")}, + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc " + "Kappa", + {emoteAt(127, "Kappa")}, + }, + }; + + for (const auto &test : testCases) + { + auto message = test.input; + auto emotes = test.twitchEmotes; + processIgnorePhrases(test.phrases, message, emotes); + + EXPECT_EQ(message, test.expectedMessage) + << "Message not equal for input '" << test.input + << "' - expected: '" << test.expectedMessage << "' got: '" + << message << "'"; + EXPECT_EQ(emotes, test.expectedTwitchEmotes) + << "Twitch emotes not equal for input '" << test.input + << "' and output '" << message << "'"; + } +} diff --git a/tests/src/MessageBuilder.cpp b/tests/src/MessageBuilder.cpp index b76d78018..107d4023c 100644 --- a/tests/src/MessageBuilder.cpp +++ b/tests/src/MessageBuilder.cpp @@ -447,454 +447,6 @@ QT_WARNING_POP } // namespace -TEST(MessageBuilder, CommaSeparatedListTagParsing) -{ - struct TestCase { - QString input; - std::pair expectedOutput; - }; - - std::vector testCases{ - { - "broadcaster/1", - {"broadcaster", "1"}, - }, - { - "predictions/foo/bar/baz", - {"predictions", "foo/bar/baz"}, - }, - { - "test/", - {"test", ""}, - }, - { - "/", - {"", ""}, - }, - { - "/value", - {"", "value"}, - }, - { - "", - {"", ""}, - }, - }; - - for (const auto &test : testCases) - { - auto output = slashKeyValue(test.input); - - EXPECT_EQ(output, test.expectedOutput) - << "Input " << test.input << " failed"; - } -} - -class TestMessageBuilder : public ::testing::Test -{ -protected: - void SetUp() override - { - this->mockApplication = std::make_unique(); - } - - void TearDown() override - { - this->mockApplication.reset(); - } - - std::unique_ptr mockApplication; -}; - -TEST(MessageBuilder, BadgeInfoParsing) -{ - struct TestCase { - QByteArray input; - std::unordered_map expectedBadgeInfo; - std::vector expectedBadges; - }; - - std::vector testCases{ - { - R"(@badge-info=predictions/<<<<<<\sHEAD[15A⸝asdf/test;badges=predictions/pink-2;client-nonce=9dbb88e516edf4efb055c011f91ea0cf;color=#FF4500;display-name=もっと頑張って;emotes=;first-msg=0;flags=;id=feb00b12-4ec5-4f77-9160-667de463dab1;mod=0;room-id=99631238;subscriber=0;tmi-sent-ts=1653494874297;turbo=0;user-id=648946956;user-type= :zniksbot!zniksbot@zniksbot.tmi.twitch.tv PRIVMSG #zneix :-tags")", - { - {"predictions", R"(<<<<<<\sHEAD[15A⸝asdf/test)"}, - }, - { - Badge{"predictions", "pink-2"}, - }, - }, - { - R"(@badge-info=predictions/<<<<<<\sHEAD[15A⸝asdf/test,founder/17;badges=predictions/pink-2,vip/1,founder/0,bits/1;client-nonce=9b836e232170a9df213aefdcb458b67e;color=#696969;display-name=NotKarar;emotes=;first-msg=0;flags=;id=e00881bd-5f21-4993-8bbd-1736cd13d42e;mod=0;room-id=99631238;subscriber=1;tmi-sent-ts=1653494879409;turbo=0;user-id=89954186;user-type= :notkarar!notkarar@notkarar.tmi.twitch.tv PRIVMSG #zneix :-tags)", - { - {"predictions", R"(<<<<<<\sHEAD[15A⸝asdf/test)"}, - {"founder", "17"}, - }, - { - Badge{"predictions", "pink-2"}, - Badge{"vip", "1"}, - Badge{"founder", "0"}, - Badge{"bits", "1"}, - }, - }, - { - R"(@badge-info=predictions/foo/bar/baz;badges=predictions/blue-1,moderator/1,glhf-pledge/1;client-nonce=f73f16228e6e32f8e92b47ab8283b7e1;color=#1E90FF;display-name=zneixbot;emotes=30259:6-12;first-msg=0;flags=;id=9682a5f1-a0b0-45e2-be9f-8074b58c5f8f;mod=1;room-id=99631238;subscriber=0;tmi-sent-ts=1653573594035;turbo=0;user-id=463521670;user-type=mod :zneixbot!zneixbot@zneixbot.tmi.twitch.tv PRIVMSG #zneix :-tags HeyGuys)", - { - {"predictions", "foo/bar/baz"}, - }, - { - Badge{"predictions", "blue-1"}, - Badge{"moderator", "1"}, - Badge{"glhf-pledge", "1"}, - }, - }, - { - R"(@badge-info=subscriber/22;badges=broadcaster/1,subscriber/18,glhf-pledge/1;color=#F97304;display-name=zneix;emotes=;first-msg=0;flags=;id=1d99f67f-a566-4416-a4e2-e85d7fce9223;mod=0;room-id=99631238;subscriber=1;tmi-sent-ts=1653612232758;turbo=0;user-id=99631238;user-type= :zneix!zneix@zneix.tmi.twitch.tv PRIVMSG #zneix :-tags)", - { - {"subscriber", "22"}, - }, - { - Badge{"broadcaster", "1"}, - Badge{"subscriber", "18"}, - Badge{"glhf-pledge", "1"}, - }, - }, - }; - - for (const auto &test : testCases) - { - auto *privmsg = - Communi::IrcPrivateMessage::fromData(test.input, nullptr); - - auto outputBadgeInfo = - MessageBuilder::parseBadgeInfoTag(privmsg->tags()); - EXPECT_EQ(outputBadgeInfo, test.expectedBadgeInfo) - << "Input for badgeInfo " << test.input << " failed"; - - auto outputBadges = MessageBuilder::parseBadgeTag(privmsg->tags()); - EXPECT_EQ(outputBadges, test.expectedBadges) - << "Input for badges " << test.input << " failed"; - - delete privmsg; - } -} - -TEST_F(TestMessageBuilder, ParseTwitchEmotes) -{ - struct TestCase { - QByteArray input; - std::vector expectedTwitchEmotes; - }; - - auto *twitchEmotes = this->mockApplication->getEmotes()->getTwitchEmotes(); - - std::vector testCases{ - { - // action /me message - R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=90ef1e46-8baa-4bf2-9c54-272f39d6fa11;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662206235860;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :ACTION Kappa)", - { - {{ - 0, // start - 4, // end - twitchEmotes->getOrCreateEmote(EmoteId{"25"}, - EmoteName{"Kappa"}), // ptr - EmoteName{"Kappa"}, // name - }}, - }, - }, - { - R"(@badge-info=subscriber/17;badges=subscriber/12,no_audio/1;color=#EBA2C0;display-name=jammehcow;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=9c2dd916-5a6d-4c1f-9fe7-a081b62a9c6b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662201093248;turbo=0;user-id=82674227;user-type= :jammehcow!jammehcow@jammehcow.tmi.twitch.tv PRIVMSG #pajlada :Kappa)", - { - {{ - 0, // start - 4, // end - twitchEmotes->getOrCreateEmote(EmoteId{"25"}, - EmoteName{"Kappa"}), // ptr - EmoteName{"Kappa"}, // name - }}, - }, - }, - { - R"(@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=1902:0-4;first-msg=0;flags=;id=9b1c3cb9-7817-47ea-add1-f9d4a9b4f846;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201095690;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Keepo)", - { - {{ - 0, // start - 4, // end - twitchEmotes->getOrCreateEmote(EmoteId{"1902"}, - EmoteName{"Keepo"}), // ptr - EmoteName{"Keepo"}, // name - }}, - }, - }, - { - R"(@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=25:0-4/1902:6-10/305954156:12-19;first-msg=0;flags=;id=7be87072-bf24-4fa3-b3df-0ea6fa5f1474;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201102276;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Kappa Keepo PogChamp)", - { - { - { - 0, // start - 4, // end - twitchEmotes->getOrCreateEmote( - EmoteId{"25"}, EmoteName{"Kappa"}), // ptr - EmoteName{"Kappa"}, // name - }, - { - 6, // start - 10, // end - twitchEmotes->getOrCreateEmote( - EmoteId{"1902"}, EmoteName{"Keepo"}), // ptr - EmoteName{"Keepo"}, // name - }, - { - 12, // start - 19, // end - twitchEmotes->getOrCreateEmote( - EmoteId{"305954156"}, - EmoteName{"PogChamp"}), // ptr - EmoteName{"PogChamp"}, // name - }, - }, - }, - }, - { - R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4,6-10;first-msg=0;flags=;id=f7516287-e5d1-43ca-974e-fe0cff84400b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204375009;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa Kappa)", - { - { - { - 0, // start - 4, // end - twitchEmotes->getOrCreateEmote( - EmoteId{"25"}, EmoteName{"Kappa"}), // ptr - EmoteName{"Kappa"}, // name - }, - { - 6, // start - 10, // end - twitchEmotes->getOrCreateEmote( - EmoteId{"25"}, EmoteName{"Kappa"}), // ptr - EmoteName{"Kappa"}, // name - }, - }, - }, - }, - { - R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emotes=25:0-4,8-12;first-msg=0;flags=;id=44f85d39-b5fb-475d-8555-f4244f2f7e82;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204423418;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa 😂 Kappa)", - { - { - { - 0, // start - 4, // end - twitchEmotes->getOrCreateEmote( - EmoteId{"25"}, EmoteName{"Kappa"}), // ptr - EmoteName{"Kappa"}, // name - }, - { - 9, // start - modified due to emoji - 13, // end - modified due to emoji - twitchEmotes->getOrCreateEmote( - EmoteId{"25"}, EmoteName{"Kappa"}), // ptr - EmoteName{"Kappa"}, // name - }, - }, - }, - }, - { - // start out of range - R"(@emotes=84608:9-10 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", - {}, - }, - { - // one character emote - R"(@emotes=84608:0-0 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", - { - { - 0, // start - 0, // end - twitchEmotes->getOrCreateEmote(EmoteId{"84608"}, - EmoteName{"f"}), // ptr - EmoteName{"f"}, // name - }, - }, - }, - { - // two character emote - R"(@emotes=84609:0-1 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", - { - { - 0, // start - 1, // end - twitchEmotes->getOrCreateEmote(EmoteId{"84609"}, - EmoteName{"fo"}), // ptr - EmoteName{"fo"}, // name - }, - }, - }, - { - // end out of range - R"(@emotes=84608:0-15 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", - {}, - }, - { - // range bad (end character before start) - R"(@emotes=84608:15-2 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", - {}, - }, - }; - - for (const auto &test : testCases) - { - auto *privmsg = dynamic_cast( - Communi::IrcPrivateMessage::fromData(test.input, nullptr)); - QString originalMessage = privmsg->content(); - - // TODO: Add tests with replies - auto actualTwitchEmotes = MessageBuilder::parseTwitchEmotes( - privmsg->tags(), originalMessage, 0); - - EXPECT_EQ(actualTwitchEmotes, test.expectedTwitchEmotes) - << "Input for twitch emotes " << test.input << " failed"; - - delete privmsg; - } -} - -TEST_F(TestMessageBuilder, IgnoresReplace) -{ - struct TestCase { - std::vector phrases; - QString input; - std::vector twitchEmotes; - QString expectedMessage; - std::vector expectedTwitchEmotes; - }; - - auto *twitchEmotes = this->mockApplication->getEmotes()->getTwitchEmotes(); - - auto emoteAt = [&](int at, const QString &name) { - return TwitchEmoteOccurrence{ - .start = at, - .end = static_cast(at + name.size() - 1), - .ptr = - twitchEmotes->getOrCreateEmote(EmoteId{name}, EmoteName{name}), - .name = EmoteName{name}, - }; - }; - - auto regularReplace = [](auto pattern, auto replace, - bool caseSensitive = true) { - return IgnorePhrase(pattern, false, false, replace, caseSensitive); - }; - auto regexReplace = [](auto pattern, auto regex, - bool caseSensitive = true) { - return IgnorePhrase(pattern, true, false, regex, caseSensitive); - }; - - std::vector testCases{ - { - {regularReplace("foo1", "baz1")}, - "foo1 Kappa", - {emoteAt(4, "Kappa")}, - "baz1 Kappa", - {emoteAt(4, "Kappa")}, - }, - { - {regularReplace("foo1", "baz1", false)}, - "FoO1 Kappa", - {emoteAt(4, "Kappa")}, - "baz1 Kappa", - {emoteAt(4, "Kappa")}, - }, - { - {regexReplace("f(o+)1", "baz1[\\1]")}, - "foo1 Kappa", - {emoteAt(4, "Kappa")}, - "baz1[oo] Kappa", - {emoteAt(8, "Kappa")}, - }, - - { - {regexReplace("f(o+)1", R"(baz1[\0][\1][\2])")}, - "foo1 Kappa", - {emoteAt(4, "Kappa")}, - "baz1[\\0][oo][\\2] Kappa", - {emoteAt(16, "Kappa")}, - }, - { - {regexReplace("f(o+)(\\d+)", "baz1[\\1+\\2]")}, - "foo123 Kappa", - {emoteAt(6, "Kappa")}, - "baz1[oo+123] Kappa", - {emoteAt(12, "Kappa")}, - }, - { - {regexReplace("(?<=foo)(\\d+)", "[\\1]")}, - "foo123 Kappa", - {emoteAt(6, "Kappa")}, - "foo[123] Kappa", - {emoteAt(8, "Kappa")}, - }, - { - {regexReplace("a(?=a| )", "b")}, - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - "aaaa" - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " - "Kappa", - {emoteAt(127, "Kappa")}, - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - "bbbb" - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb " - "Kappa", - {emoteAt(127, "Kappa")}, - }, - { - {regexReplace("abc", "def", false)}, - "AbC Kappa", - {emoteAt(3, "Kappa")}, - "def Kappa", - {emoteAt(3, "Kappa")}, - }, - { - { - regexReplace("abc", "def", false), - regularReplace("def", "ghi"), - }, - "AbC Kappa", - {emoteAt(3, "Kappa")}, - "ghi Kappa", - {emoteAt(3, "Kappa")}, - }, - { - { - regexReplace("a(?=a| )", "b"), - regexReplace("b(?=b| )", "c"), - }, - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - "aaaa" - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " - "Kappa", - {emoteAt(127, "Kappa")}, - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc " - "Kappa", - {emoteAt(127, "Kappa")}, - }, - }; - - for (const auto &test : testCases) - { - auto message = test.input; - auto emotes = test.twitchEmotes; - MessageBuilder::processIgnorePhrases(test.phrases, message, emotes); - - EXPECT_EQ(message, test.expectedMessage) - << "Message not equal for input '" << test.input - << "' - expected: '" << test.expectedMessage << "' got: '" - << message << "'"; - EXPECT_EQ(emotes, test.expectedTwitchEmotes) - << "Twitch emotes not equal for input '" << test.input - << "' and output '" << message << "'"; - } -} - class TestMessageBuilderP : public ::testing::TestWithParam { public: diff --git a/tests/src/TwitchIrc.cpp b/tests/src/TwitchIrc.cpp new file mode 100644 index 000000000..8403e33a0 --- /dev/null +++ b/tests/src/TwitchIrc.cpp @@ -0,0 +1,336 @@ +#include "providers/twitch/TwitchIrc.hpp" + +#include "mocks/BaseApplication.hpp" +#include "mocks/Emotes.hpp" +#include "providers/twitch/TwitchBadge.hpp" +#include "Test.hpp" +#include "util/IrcHelpers.hpp" + +using namespace chatterino; + +namespace { + +class MockApplication : public mock::BaseApplication +{ +public: + MockApplication() = default; + + IEmotes *getEmotes() override + { + return &this->emotes; + } + + mock::Emotes emotes; +}; + +} // namespace + +class TestTwitchIrc : public ::testing::Test +{ +protected: + void SetUp() override + { + this->mockApplication = std::make_unique(); + } + + void TearDown() override + { + this->mockApplication.reset(); + } + + std::unique_ptr mockApplication; +}; + +TEST(TwitchIrc, CommaSeparatedListTagParsing) +{ + struct TestCase { + QString input; + std::pair expectedOutput; + }; + + std::vector testCases{ + { + "broadcaster/1", + {"broadcaster", "1"}, + }, + { + "predictions/foo/bar/baz", + {"predictions", "foo/bar/baz"}, + }, + { + "test/", + {"test", ""}, + }, + { + "/", + {"", ""}, + }, + { + "/value", + {"", "value"}, + }, + { + "", + {"", ""}, + }, + }; + + for (const auto &test : testCases) + { + auto output = slashKeyValue(test.input); + + EXPECT_EQ(output, test.expectedOutput) + << "Input " << test.input << " failed"; + } +} + +TEST(TwitchIrc, BadgeInfoParsing) +{ + struct TestCase { + QByteArray input; + std::unordered_map expectedBadgeInfo; + std::vector expectedBadges; + }; + + std::vector testCases{ + { + R"(@badge-info=predictions/<<<<<<\sHEAD[15A⸝asdf/test;badges=predictions/pink-2;client-nonce=9dbb88e516edf4efb055c011f91ea0cf;color=#FF4500;display-name=もっと頑張って;emotes=;first-msg=0;flags=;id=feb00b12-4ec5-4f77-9160-667de463dab1;mod=0;room-id=99631238;subscriber=0;tmi-sent-ts=1653494874297;turbo=0;user-id=648946956;user-type= :zniksbot!zniksbot@zniksbot.tmi.twitch.tv PRIVMSG #zneix :-tags")", + { + {"predictions", R"(<<<<<<\sHEAD[15A⸝asdf/test)"}, + }, + { + Badge{"predictions", "pink-2"}, + }, + }, + { + R"(@badge-info=predictions/<<<<<<\sHEAD[15A⸝asdf/test,founder/17;badges=predictions/pink-2,vip/1,founder/0,bits/1;client-nonce=9b836e232170a9df213aefdcb458b67e;color=#696969;display-name=NotKarar;emotes=;first-msg=0;flags=;id=e00881bd-5f21-4993-8bbd-1736cd13d42e;mod=0;room-id=99631238;subscriber=1;tmi-sent-ts=1653494879409;turbo=0;user-id=89954186;user-type= :notkarar!notkarar@notkarar.tmi.twitch.tv PRIVMSG #zneix :-tags)", + { + {"predictions", R"(<<<<<<\sHEAD[15A⸝asdf/test)"}, + {"founder", "17"}, + }, + { + Badge{"predictions", "pink-2"}, + Badge{"vip", "1"}, + Badge{"founder", "0"}, + Badge{"bits", "1"}, + }, + }, + { + R"(@badge-info=predictions/foo/bar/baz;badges=predictions/blue-1,moderator/1,glhf-pledge/1;client-nonce=f73f16228e6e32f8e92b47ab8283b7e1;color=#1E90FF;display-name=zneixbot;emotes=30259:6-12;first-msg=0;flags=;id=9682a5f1-a0b0-45e2-be9f-8074b58c5f8f;mod=1;room-id=99631238;subscriber=0;tmi-sent-ts=1653573594035;turbo=0;user-id=463521670;user-type=mod :zneixbot!zneixbot@zneixbot.tmi.twitch.tv PRIVMSG #zneix :-tags HeyGuys)", + { + {"predictions", "foo/bar/baz"}, + }, + { + Badge{"predictions", "blue-1"}, + Badge{"moderator", "1"}, + Badge{"glhf-pledge", "1"}, + }, + }, + { + R"(@badge-info=subscriber/22;badges=broadcaster/1,subscriber/18,glhf-pledge/1;color=#F97304;display-name=zneix;emotes=;first-msg=0;flags=;id=1d99f67f-a566-4416-a4e2-e85d7fce9223;mod=0;room-id=99631238;subscriber=1;tmi-sent-ts=1653612232758;turbo=0;user-id=99631238;user-type= :zneix!zneix@zneix.tmi.twitch.tv PRIVMSG #zneix :-tags)", + { + {"subscriber", "22"}, + }, + { + Badge{"broadcaster", "1"}, + Badge{"subscriber", "18"}, + Badge{"glhf-pledge", "1"}, + }, + }, + }; + + for (const auto &test : testCases) + { + auto *privmsg = + Communi::IrcPrivateMessage::fromData(test.input, nullptr); + + auto outputBadgeInfo = parseBadgeInfoTag(privmsg->tags()); + EXPECT_EQ(outputBadgeInfo, test.expectedBadgeInfo) + << "Input for badgeInfo " << test.input << " failed"; + + auto outputBadges = parseBadgeTag(privmsg->tags()); + EXPECT_EQ(outputBadges, test.expectedBadges) + << "Input for badges " << test.input << " failed"; + + delete privmsg; + } +} + +TEST_F(TestTwitchIrc, ParseTwitchEmotes) +{ + struct TestCase { + QByteArray input; + std::vector expectedTwitchEmotes; + }; + + auto *twitchEmotes = this->mockApplication->getEmotes()->getTwitchEmotes(); + + std::vector testCases{ + { + // action /me message + R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=90ef1e46-8baa-4bf2-9c54-272f39d6fa11;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662206235860;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :ACTION Kappa)", + { + {{ + 0, // start + 4, // end + twitchEmotes->getOrCreateEmote(EmoteId{"25"}, + EmoteName{"Kappa"}), // ptr + EmoteName{"Kappa"}, // name + }}, + }, + }, + { + R"(@badge-info=subscriber/17;badges=subscriber/12,no_audio/1;color=#EBA2C0;display-name=jammehcow;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=9c2dd916-5a6d-4c1f-9fe7-a081b62a9c6b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662201093248;turbo=0;user-id=82674227;user-type= :jammehcow!jammehcow@jammehcow.tmi.twitch.tv PRIVMSG #pajlada :Kappa)", + { + {{ + 0, // start + 4, // end + twitchEmotes->getOrCreateEmote(EmoteId{"25"}, + EmoteName{"Kappa"}), // ptr + EmoteName{"Kappa"}, // name + }}, + }, + }, + { + R"(@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=1902:0-4;first-msg=0;flags=;id=9b1c3cb9-7817-47ea-add1-f9d4a9b4f846;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201095690;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Keepo)", + { + {{ + 0, // start + 4, // end + twitchEmotes->getOrCreateEmote(EmoteId{"1902"}, + EmoteName{"Keepo"}), // ptr + EmoteName{"Keepo"}, // name + }}, + }, + }, + { + R"(@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=25:0-4/1902:6-10/305954156:12-19;first-msg=0;flags=;id=7be87072-bf24-4fa3-b3df-0ea6fa5f1474;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201102276;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Kappa Keepo PogChamp)", + { + { + { + 0, // start + 4, // end + twitchEmotes->getOrCreateEmote( + EmoteId{"25"}, EmoteName{"Kappa"}), // ptr + EmoteName{"Kappa"}, // name + }, + { + 6, // start + 10, // end + twitchEmotes->getOrCreateEmote( + EmoteId{"1902"}, EmoteName{"Keepo"}), // ptr + EmoteName{"Keepo"}, // name + }, + { + 12, // start + 19, // end + twitchEmotes->getOrCreateEmote( + EmoteId{"305954156"}, + EmoteName{"PogChamp"}), // ptr + EmoteName{"PogChamp"}, // name + }, + }, + }, + }, + { + R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4,6-10;first-msg=0;flags=;id=f7516287-e5d1-43ca-974e-fe0cff84400b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204375009;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa Kappa)", + { + { + { + 0, // start + 4, // end + twitchEmotes->getOrCreateEmote( + EmoteId{"25"}, EmoteName{"Kappa"}), // ptr + EmoteName{"Kappa"}, // name + }, + { + 6, // start + 10, // end + twitchEmotes->getOrCreateEmote( + EmoteId{"25"}, EmoteName{"Kappa"}), // ptr + EmoteName{"Kappa"}, // name + }, + }, + }, + }, + { + R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emotes=25:0-4,8-12;first-msg=0;flags=;id=44f85d39-b5fb-475d-8555-f4244f2f7e82;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204423418;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa 😂 Kappa)", + { + { + { + 0, // start + 4, // end + twitchEmotes->getOrCreateEmote( + EmoteId{"25"}, EmoteName{"Kappa"}), // ptr + EmoteName{"Kappa"}, // name + }, + { + 9, // start - modified due to emoji + 13, // end - modified due to emoji + twitchEmotes->getOrCreateEmote( + EmoteId{"25"}, EmoteName{"Kappa"}), // ptr + EmoteName{"Kappa"}, // name + }, + }, + }, + }, + { + // start out of range + R"(@emotes=84608:9-10 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", + {}, + }, + { + // one character emote + R"(@emotes=84608:0-0 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", + { + { + 0, // start + 0, // end + twitchEmotes->getOrCreateEmote(EmoteId{"84608"}, + EmoteName{"f"}), // ptr + EmoteName{"f"}, // name + }, + }, + }, + { + // two character emote + R"(@emotes=84609:0-1 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", + { + { + 0, // start + 1, // end + twitchEmotes->getOrCreateEmote(EmoteId{"84609"}, + EmoteName{"fo"}), // ptr + EmoteName{"fo"}, // name + }, + }, + }, + { + // end out of range + R"(@emotes=84608:0-15 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", + {}, + }, + { + // range bad (end character before start) + R"(@emotes=84608:15-2 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)", + {}, + }, + }; + + for (const auto &test : testCases) + { + auto *privmsg = dynamic_cast( + Communi::IrcPrivateMessage::fromData(test.input, nullptr)); + ASSERT_NE(privmsg, nullptr); + QString originalMessage = privmsg->content(); + + // TODO: Add tests with replies + auto actualTwitchEmotes = + parseTwitchEmotes(privmsg->tags(), originalMessage, 0); + + EXPECT_EQ(actualTwitchEmotes, test.expectedTwitchEmotes) + << "Input for twitch emotes " << test.input << " failed"; + + delete privmsg; + } +} From a8d60b0b057127b29931578aa2d00725e3489320 Mon Sep 17 00:00:00 2001 From: nerix Date: Fri, 18 Oct 2024 15:41:57 +0200 Subject: [PATCH 13/40] refactor: move MessageBuilder snapshot test to IrcMessageHandler (#5654) --- CHANGELOG.md | 2 +- src/providers/twitch/TwitchChannel.hpp | 4 ++-- tests/CMakeLists.txt | 2 +- .../IRC => IrcMessageHandler}/action.json | 0 .../all-usernames.json | 0 .../IRC => IrcMessageHandler}/bad-emotes.json | 0 .../IRC => IrcMessageHandler}/bad-emotes2.json | 0 .../IRC => IrcMessageHandler}/bad-emotes3.json | 0 .../IRC => IrcMessageHandler}/bad-emotes4.json | 0 .../badges-invalid.json | 0 .../IRC => IrcMessageHandler}/badges.json | 0 .../IRC => IrcMessageHandler}/blocked-user.json | 0 .../IRC => IrcMessageHandler}/cheer1.json | 0 .../IRC => IrcMessageHandler}/cheer2.json | 0 .../IRC => IrcMessageHandler}/cheer3.json | 0 .../IRC => IrcMessageHandler}/cheer4.json | 0 .../IRC => IrcMessageHandler}/custom-mod.json | 0 .../IRC => IrcMessageHandler}/custom-vip.json | 0 .../IRC => IrcMessageHandler}/emote-emoji.json | 0 .../IRC => IrcMessageHandler}/emote.json | 0 .../IRC => IrcMessageHandler}/emotes.json | 0 .../IRC => IrcMessageHandler}/emotes2.json | 0 .../IRC => IrcMessageHandler}/emotes3.json | 0 .../IRC => IrcMessageHandler}/emotes4.json | 0 .../IRC => IrcMessageHandler}/emotes5.json | 0 .../IRC => IrcMessageHandler}/first-msg.json | 0 .../IRC => IrcMessageHandler}/highlight1.json | 0 .../IRC => IrcMessageHandler}/highlight2.json | 0 .../IRC => IrcMessageHandler}/highlight3.json | 0 .../hype-chat-invalid.json | 0 .../IRC => IrcMessageHandler}/hype-chat0.json | 0 .../IRC => IrcMessageHandler}/hype-chat1.json | 0 .../IRC => IrcMessageHandler}/hype-chat2.json | 0 .../ignore-block1.json | 0 .../ignore-block2.json | 0 .../ignore-infinite.json | 0 .../ignore-replace.json | 0 .../IRC => IrcMessageHandler}/justinfan.json | 0 .../IRC => IrcMessageHandler}/links.json | 0 .../IRC => IrcMessageHandler}/mentions.json | 0 .../IRC => IrcMessageHandler}/mod.json | 0 .../IRC => IrcMessageHandler}/nickname.json | 0 .../IRC => IrcMessageHandler}/no-nick.json | 0 .../IRC => IrcMessageHandler}/no-tags.json | 0 .../redeemed-highlight.json | 0 .../IRC => IrcMessageHandler}/reply-action.json | 0 .../IRC => IrcMessageHandler}/reply-block.json | 0 .../reply-blocked-user.json | 0 .../IRC => IrcMessageHandler}/reply-child.json | 0 .../IRC => IrcMessageHandler}/reply-ignore.json | 0 .../reply-no-prev.json | 0 .../IRC => IrcMessageHandler}/reply-root.json | 0 .../IRC => IrcMessageHandler}/reply-single.json | 0 .../IRC => IrcMessageHandler}/reward-bits.json | 0 .../reward-blocked-user.json | 0 .../IRC => IrcMessageHandler}/reward-empty.json | 0 .../IRC => IrcMessageHandler}/reward-known.json | 0 .../reward-unknown.json | 0 .../IRC => IrcMessageHandler}/rm-deleted.json | 0 .../shared-chat-emotes.json | 0 .../shared-chat-known.json | 0 .../shared-chat-same-channel.json | 0 .../shared-chat-unknown.json | 0 .../IRC => IrcMessageHandler}/simple.json | 0 .../username-localized.json | 0 .../username-localized2.json | 0 .../IRC => IrcMessageHandler}/username.json | 0 .../IRC => IrcMessageHandler}/vip.json | 0 ...MessageBuilder.cpp => IrcMessageHandler.cpp} | 17 ++++++++--------- 69 files changed, 12 insertions(+), 13 deletions(-) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/action.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/all-usernames.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/bad-emotes.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/bad-emotes2.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/bad-emotes3.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/bad-emotes4.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/badges-invalid.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/badges.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/blocked-user.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/cheer1.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/cheer2.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/cheer3.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/cheer4.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/custom-mod.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/custom-vip.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/emote-emoji.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/emote.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/emotes.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/emotes2.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/emotes3.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/emotes4.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/emotes5.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/first-msg.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/highlight1.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/highlight2.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/highlight3.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/hype-chat-invalid.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/hype-chat0.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/hype-chat1.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/hype-chat2.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/ignore-block1.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/ignore-block2.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/ignore-infinite.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/ignore-replace.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/justinfan.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/links.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/mentions.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/mod.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/nickname.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/no-nick.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/no-tags.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/redeemed-highlight.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reply-action.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reply-block.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reply-blocked-user.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reply-child.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reply-ignore.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reply-no-prev.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reply-root.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reply-single.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reward-bits.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reward-blocked-user.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reward-empty.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reward-known.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/reward-unknown.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/rm-deleted.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/shared-chat-emotes.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/shared-chat-known.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/shared-chat-same-channel.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/shared-chat-unknown.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/simple.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/username-localized.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/username-localized2.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/username.json (100%) rename tests/snapshots/{MessageBuilder/IRC => IrcMessageHandler}/vip.json (100%) rename tests/src/{MessageBuilder.cpp => IrcMessageHandler.cpp} (97%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54544119b..fc73acb32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,7 +103,7 @@ - Dev: Added more tests for input completion. (#5604) - Dev: Refactored legacy Unicode zero-width-joiner replacement. (#5594) - Dev: The JSON output when copying a message (SHIFT + right-click) is now more extensive. (#5600) -- Dev: Added more tests for message building. (#5598) +- Dev: Added more tests for message building. (#5598, #5654) - Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607) - Dev: `GIFTimer` is no longer initialized in tests. (#5608) - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 802da8112..41eb53de8 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -25,7 +25,7 @@ #include #include -class TestMessageBuilderP; +class TestIrcMessageHandlerP; namespace chatterino { @@ -464,7 +464,7 @@ private: friend class MessageBuilder; friend class IrcMessageHandler; friend class Commands_E2E_Test; - friend class ::TestMessageBuilderP; + friend class ::TestIrcMessageHandlerP; }; } // namespace chatterino diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8a6647fee..5a2cb5f1b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,7 +22,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/UtilTwitch.cpp ${CMAKE_CURRENT_LIST_DIR}/src/IrcHelpers.cpp ${CMAKE_CURRENT_LIST_DIR}/src/TwitchPubSubClient.cpp - ${CMAKE_CURRENT_LIST_DIR}/src/MessageBuilder.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/IrcMessageHandler.cpp ${CMAKE_CURRENT_LIST_DIR}/src/HighlightController.cpp ${CMAKE_CURRENT_LIST_DIR}/src/FormatTime.cpp ${CMAKE_CURRENT_LIST_DIR}/src/LimitedQueue.cpp diff --git a/tests/snapshots/MessageBuilder/IRC/action.json b/tests/snapshots/IrcMessageHandler/action.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/action.json rename to tests/snapshots/IrcMessageHandler/action.json diff --git a/tests/snapshots/MessageBuilder/IRC/all-usernames.json b/tests/snapshots/IrcMessageHandler/all-usernames.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/all-usernames.json rename to tests/snapshots/IrcMessageHandler/all-usernames.json diff --git a/tests/snapshots/MessageBuilder/IRC/bad-emotes.json b/tests/snapshots/IrcMessageHandler/bad-emotes.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/bad-emotes.json rename to tests/snapshots/IrcMessageHandler/bad-emotes.json diff --git a/tests/snapshots/MessageBuilder/IRC/bad-emotes2.json b/tests/snapshots/IrcMessageHandler/bad-emotes2.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/bad-emotes2.json rename to tests/snapshots/IrcMessageHandler/bad-emotes2.json diff --git a/tests/snapshots/MessageBuilder/IRC/bad-emotes3.json b/tests/snapshots/IrcMessageHandler/bad-emotes3.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/bad-emotes3.json rename to tests/snapshots/IrcMessageHandler/bad-emotes3.json diff --git a/tests/snapshots/MessageBuilder/IRC/bad-emotes4.json b/tests/snapshots/IrcMessageHandler/bad-emotes4.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/bad-emotes4.json rename to tests/snapshots/IrcMessageHandler/bad-emotes4.json diff --git a/tests/snapshots/MessageBuilder/IRC/badges-invalid.json b/tests/snapshots/IrcMessageHandler/badges-invalid.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/badges-invalid.json rename to tests/snapshots/IrcMessageHandler/badges-invalid.json diff --git a/tests/snapshots/MessageBuilder/IRC/badges.json b/tests/snapshots/IrcMessageHandler/badges.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/badges.json rename to tests/snapshots/IrcMessageHandler/badges.json diff --git a/tests/snapshots/MessageBuilder/IRC/blocked-user.json b/tests/snapshots/IrcMessageHandler/blocked-user.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/blocked-user.json rename to tests/snapshots/IrcMessageHandler/blocked-user.json diff --git a/tests/snapshots/MessageBuilder/IRC/cheer1.json b/tests/snapshots/IrcMessageHandler/cheer1.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/cheer1.json rename to tests/snapshots/IrcMessageHandler/cheer1.json diff --git a/tests/snapshots/MessageBuilder/IRC/cheer2.json b/tests/snapshots/IrcMessageHandler/cheer2.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/cheer2.json rename to tests/snapshots/IrcMessageHandler/cheer2.json diff --git a/tests/snapshots/MessageBuilder/IRC/cheer3.json b/tests/snapshots/IrcMessageHandler/cheer3.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/cheer3.json rename to tests/snapshots/IrcMessageHandler/cheer3.json diff --git a/tests/snapshots/MessageBuilder/IRC/cheer4.json b/tests/snapshots/IrcMessageHandler/cheer4.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/cheer4.json rename to tests/snapshots/IrcMessageHandler/cheer4.json diff --git a/tests/snapshots/MessageBuilder/IRC/custom-mod.json b/tests/snapshots/IrcMessageHandler/custom-mod.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/custom-mod.json rename to tests/snapshots/IrcMessageHandler/custom-mod.json diff --git a/tests/snapshots/MessageBuilder/IRC/custom-vip.json b/tests/snapshots/IrcMessageHandler/custom-vip.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/custom-vip.json rename to tests/snapshots/IrcMessageHandler/custom-vip.json diff --git a/tests/snapshots/MessageBuilder/IRC/emote-emoji.json b/tests/snapshots/IrcMessageHandler/emote-emoji.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/emote-emoji.json rename to tests/snapshots/IrcMessageHandler/emote-emoji.json diff --git a/tests/snapshots/MessageBuilder/IRC/emote.json b/tests/snapshots/IrcMessageHandler/emote.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/emote.json rename to tests/snapshots/IrcMessageHandler/emote.json diff --git a/tests/snapshots/MessageBuilder/IRC/emotes.json b/tests/snapshots/IrcMessageHandler/emotes.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/emotes.json rename to tests/snapshots/IrcMessageHandler/emotes.json diff --git a/tests/snapshots/MessageBuilder/IRC/emotes2.json b/tests/snapshots/IrcMessageHandler/emotes2.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/emotes2.json rename to tests/snapshots/IrcMessageHandler/emotes2.json diff --git a/tests/snapshots/MessageBuilder/IRC/emotes3.json b/tests/snapshots/IrcMessageHandler/emotes3.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/emotes3.json rename to tests/snapshots/IrcMessageHandler/emotes3.json diff --git a/tests/snapshots/MessageBuilder/IRC/emotes4.json b/tests/snapshots/IrcMessageHandler/emotes4.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/emotes4.json rename to tests/snapshots/IrcMessageHandler/emotes4.json diff --git a/tests/snapshots/MessageBuilder/IRC/emotes5.json b/tests/snapshots/IrcMessageHandler/emotes5.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/emotes5.json rename to tests/snapshots/IrcMessageHandler/emotes5.json diff --git a/tests/snapshots/MessageBuilder/IRC/first-msg.json b/tests/snapshots/IrcMessageHandler/first-msg.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/first-msg.json rename to tests/snapshots/IrcMessageHandler/first-msg.json diff --git a/tests/snapshots/MessageBuilder/IRC/highlight1.json b/tests/snapshots/IrcMessageHandler/highlight1.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/highlight1.json rename to tests/snapshots/IrcMessageHandler/highlight1.json diff --git a/tests/snapshots/MessageBuilder/IRC/highlight2.json b/tests/snapshots/IrcMessageHandler/highlight2.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/highlight2.json rename to tests/snapshots/IrcMessageHandler/highlight2.json diff --git a/tests/snapshots/MessageBuilder/IRC/highlight3.json b/tests/snapshots/IrcMessageHandler/highlight3.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/highlight3.json rename to tests/snapshots/IrcMessageHandler/highlight3.json diff --git a/tests/snapshots/MessageBuilder/IRC/hype-chat-invalid.json b/tests/snapshots/IrcMessageHandler/hype-chat-invalid.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/hype-chat-invalid.json rename to tests/snapshots/IrcMessageHandler/hype-chat-invalid.json diff --git a/tests/snapshots/MessageBuilder/IRC/hype-chat0.json b/tests/snapshots/IrcMessageHandler/hype-chat0.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/hype-chat0.json rename to tests/snapshots/IrcMessageHandler/hype-chat0.json diff --git a/tests/snapshots/MessageBuilder/IRC/hype-chat1.json b/tests/snapshots/IrcMessageHandler/hype-chat1.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/hype-chat1.json rename to tests/snapshots/IrcMessageHandler/hype-chat1.json diff --git a/tests/snapshots/MessageBuilder/IRC/hype-chat2.json b/tests/snapshots/IrcMessageHandler/hype-chat2.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/hype-chat2.json rename to tests/snapshots/IrcMessageHandler/hype-chat2.json diff --git a/tests/snapshots/MessageBuilder/IRC/ignore-block1.json b/tests/snapshots/IrcMessageHandler/ignore-block1.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/ignore-block1.json rename to tests/snapshots/IrcMessageHandler/ignore-block1.json diff --git a/tests/snapshots/MessageBuilder/IRC/ignore-block2.json b/tests/snapshots/IrcMessageHandler/ignore-block2.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/ignore-block2.json rename to tests/snapshots/IrcMessageHandler/ignore-block2.json diff --git a/tests/snapshots/MessageBuilder/IRC/ignore-infinite.json b/tests/snapshots/IrcMessageHandler/ignore-infinite.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/ignore-infinite.json rename to tests/snapshots/IrcMessageHandler/ignore-infinite.json diff --git a/tests/snapshots/MessageBuilder/IRC/ignore-replace.json b/tests/snapshots/IrcMessageHandler/ignore-replace.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/ignore-replace.json rename to tests/snapshots/IrcMessageHandler/ignore-replace.json diff --git a/tests/snapshots/MessageBuilder/IRC/justinfan.json b/tests/snapshots/IrcMessageHandler/justinfan.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/justinfan.json rename to tests/snapshots/IrcMessageHandler/justinfan.json diff --git a/tests/snapshots/MessageBuilder/IRC/links.json b/tests/snapshots/IrcMessageHandler/links.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/links.json rename to tests/snapshots/IrcMessageHandler/links.json diff --git a/tests/snapshots/MessageBuilder/IRC/mentions.json b/tests/snapshots/IrcMessageHandler/mentions.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/mentions.json rename to tests/snapshots/IrcMessageHandler/mentions.json diff --git a/tests/snapshots/MessageBuilder/IRC/mod.json b/tests/snapshots/IrcMessageHandler/mod.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/mod.json rename to tests/snapshots/IrcMessageHandler/mod.json diff --git a/tests/snapshots/MessageBuilder/IRC/nickname.json b/tests/snapshots/IrcMessageHandler/nickname.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/nickname.json rename to tests/snapshots/IrcMessageHandler/nickname.json diff --git a/tests/snapshots/MessageBuilder/IRC/no-nick.json b/tests/snapshots/IrcMessageHandler/no-nick.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/no-nick.json rename to tests/snapshots/IrcMessageHandler/no-nick.json diff --git a/tests/snapshots/MessageBuilder/IRC/no-tags.json b/tests/snapshots/IrcMessageHandler/no-tags.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/no-tags.json rename to tests/snapshots/IrcMessageHandler/no-tags.json diff --git a/tests/snapshots/MessageBuilder/IRC/redeemed-highlight.json b/tests/snapshots/IrcMessageHandler/redeemed-highlight.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/redeemed-highlight.json rename to tests/snapshots/IrcMessageHandler/redeemed-highlight.json diff --git a/tests/snapshots/MessageBuilder/IRC/reply-action.json b/tests/snapshots/IrcMessageHandler/reply-action.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reply-action.json rename to tests/snapshots/IrcMessageHandler/reply-action.json diff --git a/tests/snapshots/MessageBuilder/IRC/reply-block.json b/tests/snapshots/IrcMessageHandler/reply-block.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reply-block.json rename to tests/snapshots/IrcMessageHandler/reply-block.json diff --git a/tests/snapshots/MessageBuilder/IRC/reply-blocked-user.json b/tests/snapshots/IrcMessageHandler/reply-blocked-user.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reply-blocked-user.json rename to tests/snapshots/IrcMessageHandler/reply-blocked-user.json diff --git a/tests/snapshots/MessageBuilder/IRC/reply-child.json b/tests/snapshots/IrcMessageHandler/reply-child.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reply-child.json rename to tests/snapshots/IrcMessageHandler/reply-child.json diff --git a/tests/snapshots/MessageBuilder/IRC/reply-ignore.json b/tests/snapshots/IrcMessageHandler/reply-ignore.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reply-ignore.json rename to tests/snapshots/IrcMessageHandler/reply-ignore.json diff --git a/tests/snapshots/MessageBuilder/IRC/reply-no-prev.json b/tests/snapshots/IrcMessageHandler/reply-no-prev.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reply-no-prev.json rename to tests/snapshots/IrcMessageHandler/reply-no-prev.json diff --git a/tests/snapshots/MessageBuilder/IRC/reply-root.json b/tests/snapshots/IrcMessageHandler/reply-root.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reply-root.json rename to tests/snapshots/IrcMessageHandler/reply-root.json diff --git a/tests/snapshots/MessageBuilder/IRC/reply-single.json b/tests/snapshots/IrcMessageHandler/reply-single.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reply-single.json rename to tests/snapshots/IrcMessageHandler/reply-single.json diff --git a/tests/snapshots/MessageBuilder/IRC/reward-bits.json b/tests/snapshots/IrcMessageHandler/reward-bits.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reward-bits.json rename to tests/snapshots/IrcMessageHandler/reward-bits.json diff --git a/tests/snapshots/MessageBuilder/IRC/reward-blocked-user.json b/tests/snapshots/IrcMessageHandler/reward-blocked-user.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reward-blocked-user.json rename to tests/snapshots/IrcMessageHandler/reward-blocked-user.json diff --git a/tests/snapshots/MessageBuilder/IRC/reward-empty.json b/tests/snapshots/IrcMessageHandler/reward-empty.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reward-empty.json rename to tests/snapshots/IrcMessageHandler/reward-empty.json diff --git a/tests/snapshots/MessageBuilder/IRC/reward-known.json b/tests/snapshots/IrcMessageHandler/reward-known.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reward-known.json rename to tests/snapshots/IrcMessageHandler/reward-known.json diff --git a/tests/snapshots/MessageBuilder/IRC/reward-unknown.json b/tests/snapshots/IrcMessageHandler/reward-unknown.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/reward-unknown.json rename to tests/snapshots/IrcMessageHandler/reward-unknown.json diff --git a/tests/snapshots/MessageBuilder/IRC/rm-deleted.json b/tests/snapshots/IrcMessageHandler/rm-deleted.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/rm-deleted.json rename to tests/snapshots/IrcMessageHandler/rm-deleted.json diff --git a/tests/snapshots/MessageBuilder/IRC/shared-chat-emotes.json b/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/shared-chat-emotes.json rename to tests/snapshots/IrcMessageHandler/shared-chat-emotes.json diff --git a/tests/snapshots/MessageBuilder/IRC/shared-chat-known.json b/tests/snapshots/IrcMessageHandler/shared-chat-known.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/shared-chat-known.json rename to tests/snapshots/IrcMessageHandler/shared-chat-known.json diff --git a/tests/snapshots/MessageBuilder/IRC/shared-chat-same-channel.json b/tests/snapshots/IrcMessageHandler/shared-chat-same-channel.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/shared-chat-same-channel.json rename to tests/snapshots/IrcMessageHandler/shared-chat-same-channel.json diff --git a/tests/snapshots/MessageBuilder/IRC/shared-chat-unknown.json b/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/shared-chat-unknown.json rename to tests/snapshots/IrcMessageHandler/shared-chat-unknown.json diff --git a/tests/snapshots/MessageBuilder/IRC/simple.json b/tests/snapshots/IrcMessageHandler/simple.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/simple.json rename to tests/snapshots/IrcMessageHandler/simple.json diff --git a/tests/snapshots/MessageBuilder/IRC/username-localized.json b/tests/snapshots/IrcMessageHandler/username-localized.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/username-localized.json rename to tests/snapshots/IrcMessageHandler/username-localized.json diff --git a/tests/snapshots/MessageBuilder/IRC/username-localized2.json b/tests/snapshots/IrcMessageHandler/username-localized2.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/username-localized2.json rename to tests/snapshots/IrcMessageHandler/username-localized2.json diff --git a/tests/snapshots/MessageBuilder/IRC/username.json b/tests/snapshots/IrcMessageHandler/username.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/username.json rename to tests/snapshots/IrcMessageHandler/username.json diff --git a/tests/snapshots/MessageBuilder/IRC/vip.json b/tests/snapshots/IrcMessageHandler/vip.json similarity index 100% rename from tests/snapshots/MessageBuilder/IRC/vip.json rename to tests/snapshots/IrcMessageHandler/vip.json diff --git a/tests/src/MessageBuilder.cpp b/tests/src/IrcMessageHandler.cpp similarity index 97% rename from tests/src/MessageBuilder.cpp rename to tests/src/IrcMessageHandler.cpp index 107d4023c..a80e928ef 100644 --- a/tests/src/MessageBuilder.cpp +++ b/tests/src/IrcMessageHandler.cpp @@ -1,4 +1,4 @@ -#include "messages/MessageBuilder.hpp" +#include "providers/twitch/IrcMessageHandler.hpp" #include "common/Literals.hpp" #include "controllers/accounts/AccountController.hpp" @@ -20,7 +20,6 @@ #include "providers/seventv/SeventvBadges.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/ChannelPointReward.hpp" -#include "providers/twitch/IrcMessageHandler.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchBadge.hpp" #include "providers/twitch/TwitchBadges.hpp" @@ -56,7 +55,7 @@ namespace { /// to generate an initial snapshot. Make sure to verify the output! constexpr bool UPDATE_SNAPSHOTS = false; -const QString IRC_CATEGORY = u"MessageBuilder/IRC"_s; +const QString IRC_CATEGORY = u"IrcMessageHandler"_s; class MockApplication : public mock::BaseApplication { @@ -447,12 +446,12 @@ QT_WARNING_POP } // namespace -class TestMessageBuilderP : public ::testing::TestWithParam +class TestIrcMessageHandlerP : public ::testing::TestWithParam { public: void SetUp() override { - auto param = TestMessageBuilderP::GetParam(); + auto param = TestIrcMessageHandlerP::GetParam(); this->snapshot = testlib::Snapshot::read(IRC_CATEGORY, param); this->mockApplication = @@ -558,7 +557,7 @@ public: /// `IrcMesssageHandler` to ensure the correct (or: "real") arguments to build /// messages. /// -/// Tests are contained in `tests/snapshots/MessageBuilder/IRC`. Fixtures +/// Tests are contained in `tests/snapshots/IrcMessageHandler`. Fixtures /// consist of an object with the keys `input`, `output`, `settings` (optional), /// and `params` (optional). /// @@ -569,7 +568,7 @@ public: /// - `prevMessages`: An array of past messages (used for replies) /// - `findAllUsernames`: A boolean controlling the equally named setting /// (default: false) -TEST_P(TestMessageBuilderP, Run) +TEST_P(TestIrcMessageHandlerP, Run) { auto channel = makeMockTwitchChannel(u"pajlada"_s, *snapshot); @@ -608,10 +607,10 @@ TEST_P(TestMessageBuilderP, Run) } INSTANTIATE_TEST_SUITE_P( - IrcMessage, TestMessageBuilderP, + IrcMessage, TestIrcMessageHandlerP, testing::ValuesIn(testlib::Snapshot::discover(IRC_CATEGORY))); -TEST(TestMessageBuilderP, Integrity) +TEST(TestIrcMessageHandlerP, Integrity) { ASSERT_FALSE(UPDATE_SNAPSHOTS); // make sure fixtures are actually tested } From 85d34c8ff068b53c5031349757c6b3475d0014cf Mon Sep 17 00:00:00 2001 From: pajlada Date: Fri, 18 Oct 2024 21:23:13 +0200 Subject: [PATCH 14/40] test: use httpbox fork release (#5655) --- .github/workflows/test-macos.yml | 12 ++++++++---- .github/workflows/test-windows.yml | 14 +++++++++----- tests/README.md | 13 +++++-------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index 00e0bf10e..b35e7aea5 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -8,6 +8,7 @@ on: env: TWITCH_PUBSUB_SERVER_TAG: v1.0.7 + HTTPBOX_TAG: v0.2.1 QT_QPA_PLATFORM: minimal HOMEBREW_NO_AUTO_UPDATE: 1 HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 @@ -58,6 +59,13 @@ jobs: run: | brew install boost openssl rapidjson p7zip create-dmg cmake + - name: Install httpbox + run: | + curl -L -o httpbox.tar.xz "https://github.com/Chatterino/httpbox/releases/download/${{ env.HTTPBOX_TAG }}/httpbox-x86_64-apple-darwin.tar.xz" + tar -xJf httpbox.tar.xz + mv ./httpbox-x86_64-apple-darwin/httpbox /usr/local/bin + working-directory: /tmp + - name: Build run: | mkdir build-test @@ -83,10 +91,6 @@ jobs: curl -L -o server.key "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key" cd .. - - name: Cargo Install httpbox - run: | - cargo install --git https://github.com/kevinastone/httpbox --rev 89b971f - - name: Test timeout-minutes: 30 run: | diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index a81c69891..926d5f907 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -8,6 +8,7 @@ on: env: TWITCH_PUBSUB_SERVER_TAG: v1.0.7 + HTTPBOX_TAG: v0.2.1 QT_QPA_PLATFORM: minimal # Last known good conan version # 2.0.3 has a bug on Windows (conan-io/conan#13606) @@ -111,6 +112,13 @@ jobs: mkdir -Force build-test/bin cp "$((ls $Env:VCToolsRedistDir/onecore/x64 -Filter '*.CRT')[0].FullName)/*" build-test/bin + - name: Install httpbox + run: | + mkdir httpbox + Invoke-WebRequest -Uri "https://github.com/Chatterino/httpbox/releases/download/${{ env.HTTPBOX_TAG }}/httpbox-x86_64-pc-windows-msvc.zip" -outfile "httpbox.zip" + Expand-Archive httpbox.zip -DestinationPath httpbox + rm httpbox.zip + - name: Build run: | cmake ` @@ -139,14 +147,10 @@ jobs: Invoke-WebRequest -Uri "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key" -outfile "server.key" cd .. - - name: Cargo Install httpbox - run: | - cargo install --git https://github.com/kevinastone/httpbox --rev 89b971f - - name: Test timeout-minutes: 30 run: | - httpbox --port 9051 & + ..\httpbox\httpbox.exe --port 9051 & cd ..\pubsub-server-test .\server.exe 127.0.0.1:9050 & cd ..\build-test diff --git a/tests/README.md b/tests/README.md index 47ac1b202..e5d73c1ad 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,10 +1,7 @@ -To run all tests you will need to run the `kennethreitz/httpbin` and `ghcr.io/chatterino/twitch-pubsub-server-test:latest` docker images. +# Pre-requisites to running tests -For example: +- Download & run [httpbox](https://github.com/Chatterino/httpbox/releases/latest) + `httpbox --port 9051` -```bash -docker run --network=host --detach ghcr.io/chatterino/twitch-pubsub-server-test:latest -docker run -p 9051:80 --detach kennethreitz/httpbin -``` - -If you're unable to use docker, you can use [httpbox](https://github.com/kevinastone/httpbox) (`httpbox --port 9051`) and [Chatterino/twitch-pubsub-server-test](https://github.com/Chatterino/twitch-pubsub-server-test/releases/latest) manually. +- Download & run [twitch-pubsub-server-test](https://github.com/Chatterino/twitch-pubsub-server-test/releases/latest) + `twitch-pubsub-server-test` From d0b9fd0dc3796403fc5539f83f6192a41146edbe Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 19 Oct 2024 11:56:46 +0200 Subject: [PATCH 15/40] fix: animate emotes when overlay is open (#5659) --- CHANGELOG.md | 2 +- src/singletons/helper/GifTimer.cpp | 1 + src/singletons/helper/GifTimer.hpp | 12 ++++++++++++ src/widgets/OverlayWindow.cpp | 3 +++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc73acb32..52be2a895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - Major: Add option to show pronouns in user card. (#5442, #5583) - Major: Release plugins alpha. (#5288) - Major: Improve high-DPI support on Windows. (#4868, #5391) -- Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746, #5643) +- Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746, #5643, #5659) - Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530) - Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625) - Minor: Moved tab visibility control to a submenu, without any toggle actions. (#5530) diff --git a/src/singletons/helper/GifTimer.cpp b/src/singletons/helper/GifTimer.cpp index 07e255381..42f1d326a 100644 --- a/src/singletons/helper/GifTimer.cpp +++ b/src/singletons/helper/GifTimer.cpp @@ -26,6 +26,7 @@ void GIFTimer::initialize() QObject::connect(&this->timer, &QTimer::timeout, [this] { if (getSettings()->animationsWhenFocused && + this->openOverlayWindows_ == 0 && QApplication::activeWindow() == nullptr) { return; diff --git a/src/singletons/helper/GifTimer.hpp b/src/singletons/helper/GifTimer.hpp index ecb741be7..a8004c8c0 100644 --- a/src/singletons/helper/GifTimer.hpp +++ b/src/singletons/helper/GifTimer.hpp @@ -18,9 +18,21 @@ public: return this->position_; } + void registerOpenOverlayWindow() + { + this->openOverlayWindows_++; + } + + void unregisterOpenOverlayWindow() + { + assert(this->openOverlayWindows_ >= 1); + this->openOverlayWindows_--; + } + private: QTimer timer; long unsigned position_{}; + size_t openOverlayWindows_ = 0; }; } // namespace chatterino diff --git a/src/widgets/OverlayWindow.cpp b/src/widgets/OverlayWindow.cpp index 94811fa87..b6cff796e 100644 --- a/src/widgets/OverlayWindow.cpp +++ b/src/widgets/OverlayWindow.cpp @@ -4,6 +4,7 @@ #include "common/FlagsEnum.hpp" #include "common/Literals.hpp" #include "controllers/hotkeys/HotkeyController.hpp" +#include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" #include "widgets/BaseWidget.hpp" @@ -166,6 +167,7 @@ OverlayWindow::OverlayWindow(IndirectChannel channel, this->addShortcuts(); this->triggerFirstActivation(); + getApp()->getEmotes()->getGIFTimer().registerOpenOverlayWindow(); } OverlayWindow::~OverlayWindow() @@ -173,6 +175,7 @@ OverlayWindow::~OverlayWindow() #ifdef Q_OS_WIN ::DestroyCursor(this->sizeAllCursor_); #endif + getApp()->getEmotes()->getGIFTimer().unregisterOpenOverlayWindow(); } void OverlayWindow::applyTheme() From dab97b3235528dd2c8059a68a410953b57a330d8 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 19 Oct 2024 12:25:21 +0200 Subject: [PATCH 16/40] test: `USERNOTICE` snapshot tests (#5656) --- CHANGELOG.md | 2 +- .../IrcMessageHandler/announcement.json | 255 ++++++++++++ .../IrcMessageHandler/bitsbadge.json | 172 ++++++++ .../shared-chat-announcement.json | 313 ++++++++++++++ .../IrcMessageHandler/sub-blocked-phrase.json | 32 ++ .../IrcMessageHandler/sub-blocked-user.json | 5 + .../IrcMessageHandler/sub-first-gift.json | 307 ++++++++++++++ .../IrcMessageHandler/sub-first-time.json | 142 +++++++ .../snapshots/IrcMessageHandler/sub-gift.json | 172 ++++++++ .../IrcMessageHandler/sub-message.json | 393 ++++++++++++++++++ .../sub-multi-month-anon-gift.json | 247 +++++++++++ .../sub-multi-month-gift-count.json | 322 ++++++++++++++ .../sub-multi-month-gift.json | 217 ++++++++++ .../sub-multi-month-resub.json | 292 +++++++++++++ .../IrcMessageHandler/sub-multi-month.json | 202 +++++++++ .../IrcMessageHandler/sub-no-message.json | 292 +++++++++++++ .../IrcMessageHandler/sub-shared-chat.json | 5 + 17 files changed, 3369 insertions(+), 1 deletion(-) create mode 100644 tests/snapshots/IrcMessageHandler/announcement.json create mode 100644 tests/snapshots/IrcMessageHandler/bitsbadge.json create mode 100644 tests/snapshots/IrcMessageHandler/shared-chat-announcement.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-blocked-phrase.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-blocked-user.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-first-gift.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-first-time.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-gift.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-message.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-multi-month-anon-gift.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-multi-month-gift-count.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-multi-month-gift.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-multi-month-resub.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-multi-month.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-no-message.json create mode 100644 tests/snapshots/IrcMessageHandler/sub-shared-chat.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 52be2a895..1aa0e5a5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,7 +103,7 @@ - Dev: Added more tests for input completion. (#5604) - Dev: Refactored legacy Unicode zero-width-joiner replacement. (#5594) - Dev: The JSON output when copying a message (SHIFT + right-click) is now more extensive. (#5600) -- Dev: Added more tests for message building. (#5598, #5654) +- Dev: Added more tests for message building. (#5598, #5654, #5656) - Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607) - Dev: `GIFTimer` is no longer initialized in tests. (#5608) - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) diff --git a/tests/snapshots/IrcMessageHandler/announcement.json b/tests/snapshots/IrcMessageHandler/announcement.json new file mode 100644 index 000000000..b3ddfd2e6 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/announcement.json @@ -0,0 +1,255 @@ +{ + "input": "@badge-info=subscriber/47;badges=broadcaster/1,subscriber/3012,twitchconAmsterdam2020/1;color=#FF0000;display-name=Supinic;emotes=;flags=;id=8c26e1ab-b50c-4d9d-bc11-3fd57a941d90;login=supinic;mod=0;msg-id=announcement;msg-param-color=PRIMARY;room-id=11148817;subscriber=1;system-msg=;tmi-sent-ts=1648762219962;user-id=31400525;user-type= :tmi.twitch.tv USERNOTICE #pajlada :mm test lol", + "output": [ + { + "badgeInfos": { + "subscriber": "47" + }, + "badges": [ + "broadcaster", + "subscriber", + "twitchconAmsterdam2020" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "Supinic", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "21:30" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "21:30:19", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/3" + }, + "name": "", + "tooltip": "Broadcaster" + }, + "flags": "BadgeChannelAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Broadcaster", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "homePage": "https://www.twitchcon.com/amsterdam/?utm_source=twitch-chat&utm_medium=badge&utm_campaign=tcamsterdam20", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/ed917c9a-1a45-4340-9c64-ca8be4348c51/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/ed917c9a-1a45-4340-9c64-ca8be4348c51/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/ed917c9a-1a45-4340-9c64-ca8be4348c51/3" + }, + "name": "", + "tooltip": "TwitchCon 2020 - Amsterdam" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "TwitchCon 2020 - Amsterdam", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffff0000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "Supinic" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Supinic:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "mm" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "test" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "lol" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "8c26e1ab-b50c-4d9d-bc11-3fd57a941d90" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|Subscription", + "id": "8c26e1ab-b50c-4d9d-bc11-3fd57a941d90", + "localizedName": "", + "loginName": "supinic", + "messageText": "mm test lol", + "searchText": "supinic supinic: mm test lol ", + "serverReceivedTime": "2022-03-31T21:30:19Z", + "timeoutUser": "", + "usernameColor": "#ffff0000" + }, + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "21:30" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "21:30:19", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Announcement" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "Announcement", + "searchText": "Announcement", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/bitsbadge.json b/tests/snapshots/IrcMessageHandler/bitsbadge.json new file mode 100644 index 000000000..bd2b35324 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/bitsbadge.json @@ -0,0 +1,172 @@ +{ + "input": "@badge-info=subscriber/2;badges=subscriber/2,bits/1000;color=#FF4500;display-name=whoopiix;emotes=;flags=;id=d2b32a02-3071-4c52-b2ce-bc3716acdc44;login=whoopiix;mod=0;msg-id=bitsbadgetier;msg-param-threshold=1000;room-id=11148817;subscriber=1;system-msg=bits\\sbadge\\stier\\snotification;tmi-sent-ts=1594520403813;user-id=104252055;user-type= :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2:20" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "02:20:03", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "whoopiix" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "just" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "earned" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "new" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1K" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Bits" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "badge!" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "whoopiix just earned a new 1K Bits badge!", + "searchText": "whoopiix just earned a new 1K Bits badge!", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json b/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json new file mode 100644 index 000000000..9c6fa2f65 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json @@ -0,0 +1,313 @@ +{ + "input": "@badge-info=;badges=staff/1,raging-wolf-helm/1;color=#DAA520;display-name=lahoooo;emotes=;flags=;id=01cd601f-bc3f-49d5-ab4b-136fa9d6ec22;login=lahoooo;mod=0;msg-id=sharedchatnotice;msg-param-color=PRIMARY;room-id=11148817;source-badge-info=;source-badges=staff/1,moderator/1,bits-leader/1;source-id=4083dadc-9f20-40f9-ba92-949ebf6bc294;source-msg-id=announcement;source-room-id=1025594235;subscriber=0;system-msg=;tmi-sent-ts=1726118378465;user-id=612865661;user-type=staff;vip=0 :tmi.twitch.tv USERNOTICE #pajlada :hi this is an announcement from 1", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "staff", + "raging-wolf-helm" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "lahoooo", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "5:19" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "05:19:38", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "homePage": "https://www.twitch.tv/jobs?ref=chat_badge", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/3" + }, + "name": "", + "tooltip": "Staff" + }, + "flags": "BadgeGlobalAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Staff", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/3ff668be-59a3-4e3e-96af-e6b2908b3171/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/3ff668be-59a3-4e3e-96af-e6b2908b3171/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/3ff668be-59a3-4e3e-96af-e6b2908b3171/3" + }, + "name": "", + "tooltip": "Raging Wolf Helm" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Raging Wolf Helm", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ffdaa520", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "lahoooo" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "lahoooo:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "hi" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "this" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "is" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "an" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "announcement" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "from" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "01cd601f-bc3f-49d5-ab4b-136fa9d6ec22" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|Subscription|SharedMessage", + "id": "01cd601f-bc3f-49d5-ab4b-136fa9d6ec22", + "localizedName": "", + "loginName": "lahoooo", + "messageText": "hi this is an announcement from 1", + "searchText": "lahoooo lahoooo: hi this is an announcement from 1 ", + "serverReceivedTime": "2024-09-12T05:19:38Z", + "timeoutUser": "", + "usernameColor": "#ffdaa520" + }, + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "5:19" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "05:19:38", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Announcement" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription|SharedMessage", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "Announcement", + "searchText": "Announcement", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/sub-blocked-phrase.json b/tests/snapshots/IrcMessageHandler/sub-blocked-phrase.json new file mode 100644 index 000000000..c8c92b59c --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-blocked-phrase.json @@ -0,0 +1,32 @@ +{ + "input": "@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-months=6;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=11148817;subscriber=1;system-msg=ronni\\shas\\ssubscribed\\sfor\\s6\\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada :somethingblock!!something", + "output": [ + ], + "settings": { + "ignore": { + "phrases": [ + { + "caseSensitive": false, + "isBlock": false, + "pattern": "ignore", + "regex": false, + "replaceWith": "replace" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "block!{2,}", + "regex": true, + "replaceWith": "?" + }, + { + "caseSensitive": true, + "isBlock": true, + "pattern": "", + "regex": false, + "replaceWith": "empty" + } + ] + } + } +} diff --git a/tests/snapshots/IrcMessageHandler/sub-blocked-user.json b/tests/snapshots/IrcMessageHandler/sub-blocked-user.json new file mode 100644 index 000000000..e444b0139 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-blocked-user.json @@ -0,0 +1,5 @@ +{ + "input": "@badges=subscriber/0,premium/1;color=#0000FF;display-name=blocked;emotes=;id=fe390424-ab89-4c33-bb5a-53c6e5214b9f;login=blocked;mod=0;msg-id=sub;msg-param-months=0;msg-param-sub-plan-name=Dakotaz;msg-param-sub-plan=Prime;room-id=39298218;subscriber=0;system-msg=byebyeheart\\sjust\\ssubscribed\\swith\\sTwitch\\sPrime!;tmi-sent-ts=1528190963670;turbo=0;user-id=12345;user-type= :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + ] +} diff --git a/tests/snapshots/IrcMessageHandler/sub-first-gift.json b/tests/snapshots/IrcMessageHandler/sub-first-gift.json new file mode 100644 index 000000000..25ed044e1 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-first-gift.json @@ -0,0 +1,307 @@ +{ + "input": "@badges=subscriber/0,premium/1;color=#00FF7F;display-name=hyperbolicxd;emotes=;id=b20ef4fe-cba8-41d0-a371-6327651dc9cc;login=hyperbolicxd;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=quote_if_nam;msg-param-recipient-id=217259245;msg-param-recipient-user-name=quote_if_nam;msg-param-sender-count=1;msg-param-sub-plan-name=Channel\\sSubscription\\s(nymn_hs);msg-param-sub-plan=1000;room-id=62300805;subscriber=1;system-msg=hyperbolicxd\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\squote_if_nam!\\sThis\\sis\\stheir\\sfirst\\sGift\\sSub\\sin\\sthe\\schannel!;tmi-sent-ts=1528190938558;turbo=0;user-id=111534250;user-type= :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "9:28" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "09:28:58", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "hyperbolicxd" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "gifted" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Tier" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "sub" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "to" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "quote_if_nam!" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "This" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "is" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "their" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "first" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Gift" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Sub" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "in" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "the" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "channel!" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "hyperbolicxd gifted a Tier 1 sub to quote_if_nam! This is their first Gift Sub in the channel!", + "searchText": "hyperbolicxd gifted a Tier 1 sub to quote_if_nam! This is their first Gift Sub in the channel!", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/sub-first-time.json b/tests/snapshots/IrcMessageHandler/sub-first-time.json new file mode 100644 index 000000000..e1427a8b0 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-first-time.json @@ -0,0 +1,142 @@ +{ + "input": "@badges=subscriber/0,premium/1;color=#0000FF;display-name=byebyeheart;emotes=;id=fe390424-ab89-4c33-bb5a-53c6e5214b9f;login=byebyeheart;mod=0;msg-id=sub;msg-param-months=0;msg-param-sub-plan-name=Dakotaz;msg-param-sub-plan=Prime;room-id=39298218;subscriber=0;system-msg=byebyeheart\\sjust\\ssubscribed\\swith\\sTwitch\\sPrime!;tmi-sent-ts=1528190963670;turbo=0;user-id=131956000;user-type= :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "9:29" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "09:29:23", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "byebyeheart" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "just" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "subscribed" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "with" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Twitch" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Prime!" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "byebyeheart just subscribed with Twitch Prime!", + "searchText": "byebyeheart just subscribed with Twitch Prime!", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/sub-gift.json b/tests/snapshots/IrcMessageHandler/sub-gift.json new file mode 100644 index 000000000..738fe14e4 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-gift.json @@ -0,0 +1,172 @@ +{ + "input": "@badges=staff/1,premium/1;color=#0000FF;display-name=TWW2;emotes=;id=e9176cd8-5e22-4684-ad40-ce53c2561c5e;login=tww2;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=Mr_Woodchuck;msg-param-recipient-id=89614178;msg-param-recipient-name=mr_woodchuck;msg-param-sub-plan-name=House\\sof\\sNyoro~n;msg-param-sub-plan=1000;room-id=19571752;subscriber=0;system-msg=TWW2\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\sMr_Woodchuck!;tmi-sent-ts=1521159445153;turbo=0;user-id=13405587;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:17" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:17:25", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "TWW2" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "gifted" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Tier" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "sub" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "to" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Mr_Woodchuck!" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "TWW2 gifted a Tier 1 sub to Mr_Woodchuck!", + "searchText": "TWW2 gifted a Tier 1 sub to Mr_Woodchuck!", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/sub-message.json b/tests/snapshots/IrcMessageHandler/sub-message.json new file mode 100644 index 000000000..fd74777c5 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-message.json @@ -0,0 +1,393 @@ +{ + "input": "@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-months=6;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=11148817;subscriber=1;system-msg=ronni\\shas\\ssubscribed\\sfor\\s6\\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada :Great stream -- keep it up!", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + "staff", + "broadcaster", + "turbo" + ], + "channelName": "pajlada", + "count": 1, + "displayName": "ronni", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "23:36" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "23:36:12", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "emote": { + "homePage": "https://www.twitch.tv/jobs?ref=chat_badge", + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/3" + }, + "name": "", + "tooltip": "Staff" + }, + "flags": "BadgeGlobalAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Staff", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/3" + }, + "name": "", + "tooltip": "Broadcaster" + }, + "flags": "BadgeChannelAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Broadcaster", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "emote": { + "images": { + "1x": "https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/1", + "2x": "https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/2", + "3x": "https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/3" + }, + "name": "", + "tooltip": "Turbo" + }, + "flags": "BadgeVanity", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Turbo", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff008000", + "flags": "Username", + "link": { + "type": "UserInfo", + "value": "ronni" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "ronni:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Great" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "stream" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "--" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "keep" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "it" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "up!" + ] + }, + { + "background": "#ffa0a0a4", + "flags": "ReplyButton", + "link": { + "type": "ReplyToMessage", + "value": "db25007f-7a18-43eb-9379-80131e44d633" + }, + "padding": 2, + "tooltip": "", + "trailingSpace": true, + "type": "CircularImageElement", + "url": "" + } + ], + "flags": "Collapsed|Subscription", + "id": "db25007f-7a18-43eb-9379-80131e44d633", + "localizedName": "", + "loginName": "ronni", + "messageText": "Great stream -- keep it up!", + "searchText": "ronni ronni: Great stream -- keep it up! ", + "serverReceivedTime": "2017-10-05T23:36:12Z", + "timeoutUser": "", + "usernameColor": "#ff008000" + }, + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "23:36" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "23:36:12", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "ronni" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "has" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "subscribed" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "for" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "6" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "months!" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "ronni has subscribed for 6 months!", + "searchText": "ronni has subscribed for 6 months!", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/sub-multi-month-anon-gift.json b/tests/snapshots/IrcMessageHandler/sub-multi-month-anon-gift.json new file mode 100644 index 000000000..a98575565 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-multi-month-anon-gift.json @@ -0,0 +1,247 @@ +{ + "input": "@msg-param-goal-user-contributions=1;system-msg=An\\sanonymous\\suser\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\sMohammadrezaDH!\\s;msg-param-goal-current-contributions=2;vip=0;color=;user-id=274598607;mod=0;flags=;msg-param-months=2;historical=1;id=afa2155b-f563-4973-a5c2-e4075882bbfb;msg-param-gift-months=6;msg-id=subgift;badge-info=;msg-param-recipient-user-name=mohammadrezadh;login=ananonymousgifter;room-id=441388138;msg-param-goal-target-contributions=25;rm-received-ts=1712002037736;msg-param-recipient-id=204174899;emotes=;display-name=AnAnonymousGifter;badges=;msg-param-fun-string=FunStringFive;msg-param-goal-contribution-type=NEW_SUB_POINTS;msg-param-origin-id=8862142563198473546;msg-param-recipient-display-name=MohammadrezaDH;msg-param-sub-plan-name=jmarxists;user-type=;subscriber=0;tmi-sent-ts=1712002037615;msg-param-sub-plan=1000;msg-param-goal-description=day\\slee\\sgoal\\s:-) :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:07" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:07:17", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "An" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "anonymous" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "user" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "gifted" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "6" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "months" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "of" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Tier" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "sub" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "to" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "MohammadrezaDH!" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "An anonymous user gifted 6 months of a Tier 1 sub to MohammadrezaDH!", + "searchText": "An anonymous user gifted 6 months of a Tier 1 sub to MohammadrezaDH!", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/sub-multi-month-gift-count.json b/tests/snapshots/IrcMessageHandler/sub-multi-month-gift-count.json new file mode 100644 index 000000000..3b9d3b106 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-multi-month-gift-count.json @@ -0,0 +1,322 @@ +{ + "input": "@badge-info=subscriber/32;badges=subscriber/3030,sub-gift-leader/2;color=#FF8EA3;display-name=iNatsuFN;emotes=;flags=;id=0d0decbd-b8f4-4e83-9e18-eca9cab69153;login=inatsufn;mod=0;msg-id=subgift;msg-param-gift-months=6;msg-param-goal-contribution-type=SUBS;msg-param-goal-current-contributions=881;msg-param-goal-target-contributions=900;msg-param-goal-user-contributions=1;msg-param-months=16;msg-param-origin-id=2524053421157386961;msg-param-recipient-display-name=kimmi_tm;msg-param-recipient-id=225806893;msg-param-recipient-user-name=kimmi_tm;msg-param-sender-count=334;msg-param-sub-plan-name=Channel\\sSubscription\\s(mxddy);msg-param-sub-plan=1000;room-id=210915729;subscriber=1;system-msg=iNatsuFN\\sgifted\\s6\\smonths\\sof\\sTier\\s1\\sto\\skimmi_tm.\\sThey've\\sgifted\\s334\\smonths\\sin\\sthe\\schannel!;tmi-sent-ts=1712034497332;user-id=218205938;user-type=;vip=0 :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "5:08" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "05:08:17", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "iNatsuFN" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "gifted" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "6" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "months" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "of" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Tier" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "sub" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "to" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "kimmi_tm!" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "They've" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "gifted" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "334" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "months" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "in" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "the" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "channel." + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "iNatsuFN gifted 6 months of a Tier 1 sub to kimmi_tm! They've gifted 334 months in the channel.", + "searchText": "iNatsuFN gifted 6 months of a Tier 1 sub to kimmi_tm! They've gifted 334 months in the channel.", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/sub-multi-month-gift.json b/tests/snapshots/IrcMessageHandler/sub-multi-month-gift.json new file mode 100644 index 000000000..65246a879 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-multi-month-gift.json @@ -0,0 +1,217 @@ +{ + "input": "@user-id=35759863;msg-param-origin-id=2862055070165643340;display-name=Lucidfoxx;id=eeb3cdb8-337c-413a-9521-3a884ff78754;msg-param-gift-months=12;msg-param-sub-plan=1000;vip=0;emotes=;badges=broadcaster/1,subscriber/3042,partner/1;msg-param-recipient-user-name=ogprodigy;msg-param-recipient-id=53888434;badge-info=subscriber/71;room-id=35759863;msg-param-recipient-display-name=OGprodigy;msg-param-sub-plan-name=Silver\\sPackage;subscriber=1;system-msg=Lucidfoxx\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\sOGprodigy!;login=lucidfoxx;msg-param-sender-count=0;user-type=;mod=0;flags=;rm-received-ts=1712803947891;color=#EB078D;msg-param-months=15;tmi-sent-ts=1712803947773;msg-id=subgift :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2:52" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "02:52:27", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Lucidfoxx" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "gifted" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "12" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "months" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "of" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Tier" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "sub" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "to" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "OGprodigy!" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "Lucidfoxx gifted 12 months of a Tier 1 sub to OGprodigy!", + "searchText": "Lucidfoxx gifted 12 months of a Tier 1 sub to OGprodigy!", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/sub-multi-month-resub.json b/tests/snapshots/IrcMessageHandler/sub-multi-month-resub.json new file mode 100644 index 000000000..4878d3f4e --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-multi-month-resub.json @@ -0,0 +1,292 @@ +{ + "input": "@system-msg=calm__like_a_tom\\ssubscribed\\sat\\sTier\\s3.\\sThey've\\ssubscribed\\sfor\\s9\\smonths!;msg-param-cumulative-months=9;mod=0;msg-param-should-share-streak=0;msg-param-sub-plan-name=Executive\\sProducer;color=#0000FF;msg-param-months=0;msg-param-multimonth-duration=6;user-type=;flags=;msg-id=resub;user-id=609242230;room-id=11148817;msg-param-multimonth-tenure=0;msg-param-sub-plan=3000;emotes=;badge-info=subscriber/9;msg-param-was-gifted=false;id=4a6e270c-8cdb-46e9-b602-f8177a79d472;badges=subscriber/3009;display-name=calm__like_a_tom;tmi-sent-ts=1725938011176;login=calm__like_a_tom;vip=0;subscriber=1 :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "3:13" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "03:13:31", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "calm__like_a_tom" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "subscribed" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "at" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Tier" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "3" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "for" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "6" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "months" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "in" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "advance," + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "reaching" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "9" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "months" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cumulatively" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "so" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "far!" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "calm__like_a_tom subscribed at Tier 3 for 6 months in advance, reaching 9 months cumulatively so far!", + "searchText": "calm__like_a_tom subscribed at Tier 3 for 6 months in advance, reaching 9 months cumulatively so far!", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/sub-multi-month.json b/tests/snapshots/IrcMessageHandler/sub-multi-month.json new file mode 100644 index 000000000..98f7e34a4 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-multi-month.json @@ -0,0 +1,202 @@ +{ + "input": "@badge-info=subscriber/1;badges=subscriber/0;emotes=;msg-param-sub-plan=1000;msg-param-months=0;mod=0;login=foly__;room-id=11148817;flags=;user-id=441166175;msg-param-multimonth-tenure=0;msg-param-should-share-streak=0;system-msg=foly__\\ssubscribed\\sat\\sTier\\s1.;msg-id=sub;display-name=foly__;msg-param-sub-plan-name=Channel\\sSubscription\\s(k4sen);user-type=;id=327249bb-81bc-4f87-8b43-c05720a2dd64;msg-param-was-gifted=false;tmi-sent-ts=1728710962985;msg-param-cumulative-months=1;vip=0;color=;subscriber=1;msg-param-multimonth-duration=6 :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "5:29" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "05:29:22", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foly__" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "subscribed" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "at" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Tier" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "for" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "6" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "months" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "in" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "advance!" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "foly__ subscribed at Tier 1 for 6 months in advance!", + "searchText": "foly__ subscribed at Tier 1 for 6 months in advance!", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/sub-no-message.json b/tests/snapshots/IrcMessageHandler/sub-no-message.json new file mode 100644 index 000000000..01d589b90 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-no-message.json @@ -0,0 +1,292 @@ +{ + "input": "@badges=subscriber/12;color=#CC00C2;display-name=cspice;emotes=;id=6fc4c3e0-ca61-454a-84b8-5669dee69fc9;login=cspice;mod=0;msg-id=resub;msg-param-months=12;msg-param-sub-plan-name=Channel\\sSubscription\\s(forsenlol):\\s$9.99\\sSub;msg-param-sub-plan=2000;room-id=22484632;subscriber=1;system-msg=cspice\\sjust\\ssubscribed\\swith\\sa\\sTier\\s2\\ssub.\\scspice\\ssubscribed\\sfor\\s12\\smonths\\sin\\sa\\srow!;tmi-sent-ts=1528192510808;turbo=0;user-id=47894662;user-type= :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "9:55" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "09:55:10", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cspice" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "just" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "subscribed" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "with" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Tier" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "sub." + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cspice" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "subscribed" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "for" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "12" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "months" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "in" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "row!" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "cspice just subscribed with a Tier 2 sub. cspice subscribed for 12 months in a row!", + "searchText": "cspice just subscribed with a Tier 2 sub. cspice subscribed for 12 months in a row!", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/sub-shared-chat.json b/tests/snapshots/IrcMessageHandler/sub-shared-chat.json new file mode 100644 index 000000000..e5bfc2223 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/sub-shared-chat.json @@ -0,0 +1,5 @@ +{ + "input": "@user-id=818265505;source-room-id=39317849;mod=0;subscriber=1;msg-param-was-gifted=false;vip=0;msg-param-months=0;tmi-sent-ts=1729257926126;msg-param-streak-months=4;msg-param-cumulative-months=6;badge-info=subscriber/6;flags=;id=5fca3f42-9eb6-46c2-a321-57ad9508b976;source-msg-id=resub;badges=subscriber/6;login=gulgarius;system-msg=Gulgarius\\ssubscribed\\sat\\sTier\\s1.\\sThey've\\ssubscribed\\sfor\\s6\\smonths,\\scurrently\\son\\sa\\s4\\smonth\\sstreak!;display-name=Gulgarius;msg-param-sub-plan-name=Join\\sthe\\sLowkoTV\\sfamily!\\s;rm-received-ts=1729257926266;source-id=5fca3f42-9eb6-46c2-a321-57ad9508b976;msg-param-sub-plan=1000;user-type=;msg-param-multimonth-duration=4;room-id=11148817;msg-param-should-share-streak=1;msg-param-multimonth-tenure=3;source-badge-info=subscriber/6;source-badges=subscriber/6;msg-id=resub;emotes=;color=;historical=1 :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + ] +} From fb787b522645787478b79ed7021a935d0400e749 Mon Sep 17 00:00:00 2001 From: James Upjohn Date: Sat, 19 Oct 2024 11:01:22 +0000 Subject: [PATCH 17/40] feat: make raid entry username clickable (#5651) --- CHANGELOG.md | 1 + src/messages/MessageBuilder.cpp | 29 ++++++++ src/messages/MessageBuilder.hpp | 6 ++ src/providers/twitch/IrcMessageHandler.cpp | 78 +++++++++++++++++++++- 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aa0e5a5a..3c7212d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ - Minor: Added `--login ` CLI argument to specify which account to start logged in as. (#5626) - Minor: Indicate when subscriptions and resubscriptions are for multiple months. (#5642) - Minor: Proxy URL information is now included in the `/debug-env` command. (#5648) +- Minor: Make raid entry message usernames clickable. (#5651) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426, #5612) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed restricted users usernames not being clickable. (#5405) diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index e470b01ce..fca5753a8 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -461,6 +461,35 @@ MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text, this->message().searchText = text; } +MessageBuilder::MessageBuilder(RaidEntryMessageTag, const QString &text, + const QString &loginName, + const QString &displayName, + const MessageColor &userColor, const QTime &time) + : MessageBuilder() +{ + this->emplace(time); + + const QStringList textFragments = + text.split(QRegularExpression("\\s"), Qt::SkipEmptyParts); + for (const auto &word : textFragments) + { + if (word == displayName) + { + this->emplace(displayName, loginName, + MessageColor::System, userColor); + continue; + } + + this->emplace(word, MessageElementFlag::Text, + MessageColor::System); + } + + this->message().flags.set(MessageFlag::System); + this->message().flags.set(MessageFlag::DoNotTriggerNotification); + this->message().messageText = text; + this->message().searchText = text; +} + MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &timeoutUser, const QString &sourceUser, const QString &systemMessageText, int times, diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index aa2933b64..b353a8bde 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -53,6 +53,8 @@ namespace linkparser { struct SystemMessageTag { }; +struct RaidEntryMessageTag { +}; struct TimeoutMessageTag { }; struct LiveUpdatesUpdateEmoteMessageTag { @@ -67,6 +69,7 @@ struct ImageUploaderResultTag { }; const SystemMessageTag systemMessage{}; +const RaidEntryMessageTag raidEntryMessage{}; const TimeoutMessageTag timeoutMessage{}; const LiveUpdatesUpdateEmoteMessageTag liveUpdatesUpdateEmoteMessage{}; const LiveUpdatesRemoveEmoteMessageTag liveUpdatesRemoveEmoteMessage{}; @@ -109,6 +112,9 @@ public: MessageBuilder(SystemMessageTag, const QString &text, const QTime &time = QTime::currentTime()); + MessageBuilder(RaidEntryMessageTag, const QString &text, + const QString &loginName, const QString &displayName, + const MessageColor &userColor, const QTime &time); MessageBuilder(TimeoutMessageTag, const QString &timeoutUser, const QString &sourceUser, const QString &systemMessageText, int times, const QTime &time = QTime::currentTime()); diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 8bac0d9fe..9724ec213 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -531,6 +531,35 @@ std::vector parseUserNoticeMessage(Channel *channel, { messageText = "Announcement"; } + else if (msgType == "raid") + { + auto login = tags.value("login").toString(); + auto displayName = tags.value("msg-param-displayName").toString(); + + if (!login.isEmpty() && !displayName.isEmpty()) + { + MessageColor color = MessageColor::System; + if (auto colorTag = tags.value("color").value(); + colorTag.isValid()) + { + color = MessageColor(colorTag); + } + + auto b = MessageBuilder( + raidEntryMessage, parseTagString(messageText), login, + displayName, color, calculateMessageTime(message).time()); + + b->flags.set(MessageFlag::Subscription); + if (mirrored) + { + b->flags.set(MessageFlag::SharedMessage); + } + + auto newMessage = b.release(); + builtMessages.emplace_back(newMessage); + return builtMessages; + } + } else if (msgType == "subgift") { if (auto monthsIt = tags.find("msg-param-gift-months"); @@ -600,12 +629,12 @@ std::vector parseUserNoticeMessage(Channel *channel, auto b = MessageBuilder(systemMessage, parseTagString(messageText), calculateMessageTime(message).time()); - b->flags.set(MessageFlag::Subscription); if (mirrored) { b->flags.set(MessageFlag::SharedMessage); } + auto newMessage = b.release(); builtMessages.emplace_back(newMessage); } @@ -1085,6 +1114,53 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, { messageText = "Announcement"; } + else if (msgType == "raid") + { + auto login = tags.value("login").toString(); + auto displayName = tags.value("msg-param-displayName").toString(); + + if (!login.isEmpty() && !displayName.isEmpty()) + { + MessageColor color = MessageColor::System; + if (auto colorTag = tags.value("color").value(); + colorTag.isValid()) + { + color = MessageColor(colorTag); + } + + auto b = MessageBuilder( + raidEntryMessage, parseTagString(messageText), login, + displayName, color, calculateMessageTime(message).time()); + + b->flags.set(MessageFlag::Subscription); + if (mirrored) + { + b->flags.set(MessageFlag::SharedMessage); + } + auto newMessage = b.release(); + + QString channelName; + + if (message->parameters().size() < 1) + { + return; + } + + if (!trimChannelName(message->parameter(0), channelName)) + { + return; + } + + auto chan = twitchServer.getChannelOrEmpty(channelName); + + if (!chan->isEmpty()) + { + chan->addMessage(newMessage, MessageContext::Original); + } + + return; + } + } else if (msgType == "subgift") { if (auto monthsIt = tags.find("msg-param-gift-months"); From 43bea0f042cf663268d1e21a8a20b5caf57c2075 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 19 Oct 2024 11:32:52 +0000 Subject: [PATCH 18/40] chore(deps): bump lib/expected-lite from `f339d2f` to `88ee08e` (#5653) --- lib/expected-lite | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/expected-lite b/lib/expected-lite index f339d2f73..88ee08eb3 160000 --- a/lib/expected-lite +++ b/lib/expected-lite @@ -1 +1 @@ -Subproject commit f339d2f73730f8fee4412f5e4938717866ecef48 +Subproject commit 88ee08eb3c3f3627ca54b90dafd1d63a6d4da96b From 5c9b17c31a02d6fa69f61ae96a2258fedd6cb798 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 19 Oct 2024 14:04:44 +0200 Subject: [PATCH 19/40] refactor: decouple reply parsing from MessageBuilder (#5660) --- CHANGELOG.md | 1 + src/providers/twitch/IrcMessageHandler.cpp | 91 ++++++++++++++-------- 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c7212d43..80d38c0fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,7 @@ - Dev: `GIFTimer` is no longer initialized in tests. (#5608) - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) - Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652) +- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660) ## 2.5.1 diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 9724ec213..7d5bb34a4 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -123,24 +123,21 @@ int stripLeadingReplyMention(const QVariantMap &tags, QString &content) return 0; } -void updateReplyParticipatedStatus(const QVariantMap &tags, - const QString &senderLogin, - MessageBuilder &builder, - std::shared_ptr &thread, - bool isNew) +[[nodiscard]] bool shouldHighlightReplyThread( + const QVariantMap &tags, const QString &senderLogin, + std::shared_ptr &thread, bool isNew) { const auto ¤tLogin = getApp()->getAccounts()->twitch.getCurrent()->getUserName(); if (thread->subscribed()) { - builder.message().flags.set(MessageFlag::SubscribedThread); - return; + return true; } if (thread->unsubscribed()) { - return; + return false; } if (getSettings()->autoSubToParticipatedThreads) @@ -154,8 +151,7 @@ void updateReplyParticipatedStatus(const QVariantMap &tags, if (name == currentLogin) { thread->markSubscribed(); - builder.message().flags.set(MessageFlag::SubscribedThread); - return; // already marked as participated + return true; // already marked as participated } } } @@ -166,6 +162,8 @@ void updateReplyParticipatedStatus(const QVariantMap &tags, // don't set the highlight here } } + + return false; } ChannelPtr channelOrEmptyByTarget(const QString &target, @@ -242,10 +240,18 @@ QMap parseBadges(const QString &badgesString) return badges; } -void populateReply(TwitchChannel *channel, Communi::IrcMessage *message, - const std::vector &otherLoaded, - MessageBuilder &builder) +struct ReplyContext { + std::shared_ptr thread; + MessagePtr parent; + bool highlight = false; +}; + +[[nodiscard]] ReplyContext getReplyContext( + TwitchChannel *channel, Communi::IrcMessage *message, + const std::vector &otherLoaded) { + ReplyContext ctx; + const auto &tags = message->tags(); if (const auto it = tags.find("reply-thread-parent-msg-id"); it != tags.end()) @@ -259,9 +265,9 @@ void populateReply(TwitchChannel *channel, Communi::IrcMessage *message, if (owned) { // Thread already exists (has a reply) - updateReplyParticipatedStatus(tags, message->nick(), builder, - owned, false); - builder.setThread(owned); + ctx.highlight = shouldHighlightReplyThread( + tags, message->nick(), owned, false); + ctx.thread = owned; rootThread = owned; } } @@ -295,10 +301,10 @@ void populateReply(TwitchChannel *channel, Communi::IrcMessage *message, { std::shared_ptr newThread = std::make_shared(foundMessage); - updateReplyParticipatedStatus(tags, message->nick(), builder, - newThread, true); + ctx.highlight = shouldHighlightReplyThread( + tags, message->nick(), newThread, true); - builder.setThread(newThread); + ctx.thread = newThread; rootThread = newThread; // Store weak reference to thread in channel channel->addReplyThread(newThread); @@ -313,7 +319,7 @@ void populateReply(TwitchChannel *channel, Communi::IrcMessage *message, { if (rootThread) { - builder.setParent(rootThread->root()); + ctx.parent = rootThread->root(); } } else @@ -324,7 +330,7 @@ void populateReply(TwitchChannel *channel, Communi::IrcMessage *message, auto thread = parentThreadIt->second.lock(); if (thread) { - builder.setParent(thread->root()); + ctx.parent = thread->root(); } } else @@ -332,12 +338,14 @@ void populateReply(TwitchChannel *channel, Communi::IrcMessage *message, auto parent = channel->findMessage(parentID); if (parent) { - builder.setParent(parent); + ctx.parent = parent; } } } } } + + return ctx; } std::optional parseClearChatMessage( @@ -705,7 +713,13 @@ std::vector IrcMessageHandler::parseMessageWithReply( privMsg->isAction()); builder.setMessageOffset(messageOffset); - populateReply(tc, message, otherLoaded, builder); + auto replyCtx = getReplyContext(tc, message, otherLoaded); + builder.setThread(std::move(replyCtx.thread)); + builder.setParent(std::move(replyCtx.parent)); + if (replyCtx.highlight) + { + builder.message().flags.set(MessageFlag::SubscribedThread); + } if (!builder.isIgnored()) { @@ -1522,8 +1536,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, QString content = originalContent; int messageOffset = stripLeadingReplyMention(tags, content); - MessageBuilder builder(channel, message, args, content, isAction); - builder.setMessageOffset(messageOffset); + ReplyContext replyCtx; if (const auto it = tags.find("reply-thread-parent-msg-id"); it != tags.end()) @@ -1535,9 +1548,9 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, { // Thread already exists (has a reply) auto thread = threadIt->second.lock(); - updateReplyParticipatedStatus(tags, message->nick(), builder, - thread, false); - builder.setThread(thread); + replyCtx.highlight = shouldHighlightReplyThread( + tags, message->nick(), thread, false); + replyCtx.thread = thread; rootThread = thread; } else @@ -1548,10 +1561,10 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, { // Found root reply message auto newThread = std::make_shared(root); - updateReplyParticipatedStatus(tags, message->nick(), builder, - newThread, true); + replyCtx.highlight = shouldHighlightReplyThread( + tags, message->nick(), newThread, true); - builder.setThread(newThread); + replyCtx.thread = newThread; rootThread = newThread; // Store weak reference to thread in channel channel->addReplyThread(newThread); @@ -1566,7 +1579,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, { if (rootThread) { - builder.setParent(rootThread->root()); + replyCtx.parent = rootThread->root(); } } else @@ -1577,7 +1590,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, auto thread = parentThreadIt->second.lock(); if (thread) { - builder.setParent(thread->root()); + replyCtx.parent = thread->root(); } } else @@ -1585,13 +1598,23 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, auto parent = channel->findMessage(parentID); if (parent) { - builder.setParent(parent); + replyCtx.parent = parent; } } } } } + MessageBuilder builder(channel, message, args, content, isAction); + builder.setMessageOffset(messageOffset); + + builder.setThread(std::move(replyCtx.thread)); + builder.setParent(std::move(replyCtx.parent)); + if (replyCtx.highlight) + { + builder.message().flags.set(MessageFlag::SubscribedThread); + } + if (isSub || !builder.isIgnored()) { if (isSub) From 9345050868288f2a8250d38b5d29f491d175dafa Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 19 Oct 2024 20:42:37 +0200 Subject: [PATCH 20/40] fix: invalidate buffers on `WM_DPICHANGED` (#5664) --- CHANGELOG.md | 2 +- src/widgets/BaseWindow.cpp | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d38c0fb..7fc9ad991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Major: Add option to show pronouns in user card. (#5442, #5583) - Major: Release plugins alpha. (#5288) -- Major: Improve high-DPI support on Windows. (#4868, #5391) +- Major: Improve high-DPI support on Windows. (#4868, #5391, #5664) - Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746, #5643, #5659) - Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530) - Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625) diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 48a8a7be9..a82a364f2 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -876,6 +876,14 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, } break; + case WM_DPICHANGED: { + // wait for Qt to process this message + postToThread([] { + getApp()->getWindows()->invalidateChannelViewBuffers(); + }); + } + break; + case WM_NCLBUTTONDOWN: case WM_NCLBUTTONUP: { // WM_NCLBUTTON{DOWN, UP} gets called when the left mouse button From 352a4ec13281ed2064845819470cfb18391362df Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sun, 20 Oct 2024 11:57:05 +0200 Subject: [PATCH 21/40] Move plugins to Sol (#5622) Co-authored-by: Nerixyz Co-authored-by: Rasmus Karlsson --- .github/workflows/test-macos.yml | 2 +- .github/workflows/test-windows.yml | 2 +- .gitmodules | 3 + CHANGELOG.md | 1 + CMakeLists.txt | 2 + cmake/FindSol2.cmake | 21 + docs/plugin-meta.lua | 89 ++- docs/wip-plugins.md | 23 +- lib/lua/CMakeLists.txt | 88 +-- lib/lua/src | 2 +- lib/sol2 | 1 + scripts/make_luals_meta.py | 16 +- src/CMakeLists.txt | 22 +- src/PrecompiledHeader.hpp | 4 + src/common/Channel.hpp | 27 - src/controllers/plugins/LuaAPI.cpp | 262 +++----- src/controllers/plugins/LuaAPI.hpp | 35 +- src/controllers/plugins/LuaUtilities.cpp | 148 ----- src/controllers/plugins/LuaUtilities.hpp | 273 +------- src/controllers/plugins/Plugin.cpp | 18 +- src/controllers/plugins/Plugin.hpp | 55 +- src/controllers/plugins/PluginController.cpp | 292 ++++----- src/controllers/plugins/PluginController.hpp | 10 +- src/controllers/plugins/PluginPermission.hpp | 2 + src/controllers/plugins/SolTypes.cpp | 131 ++++ src/controllers/plugins/SolTypes.hpp | 170 +++++ src/controllers/plugins/api/ChannelRef.cpp | 511 +++++---------- src/controllers/plugins/api/ChannelRef.hpp | 144 +---- src/controllers/plugins/api/EventType.hpp | 14 + src/controllers/plugins/api/HTTPRequest.cpp | 479 ++++---------- src/controllers/plugins/api/HTTPRequest.hpp | 62 +- src/controllers/plugins/api/HTTPResponse.cpp | 131 +--- src/controllers/plugins/api/HTTPResponse.hpp | 39 +- src/controllers/plugins/api/IOWrapper.cpp | 394 +++++------- src/controllers/plugins/api/IOWrapper.hpp | 35 +- src/providers/twitch/TwitchChannel.hpp | 37 ++ src/util/TypeName.hpp | 9 + src/widgets/splits/SplitContainer.cpp | 28 +- tests/CMakeLists.txt | 1 + tests/src/NetworkHelpers.hpp | 55 ++ tests/src/NetworkRequest.cpp | 49 +- tests/src/Plugins.cpp | 641 +++++++++++++++++++ 42 files changed, 2120 insertions(+), 2208 deletions(-) create mode 100644 cmake/FindSol2.cmake create mode 160000 lib/sol2 create mode 100644 src/controllers/plugins/SolTypes.cpp create mode 100644 src/controllers/plugins/SolTypes.hpp create mode 100644 src/controllers/plugins/api/EventType.hpp create mode 100644 tests/src/NetworkHelpers.hpp create mode 100644 tests/src/Plugins.cpp diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index b35e7aea5..3eb1ef633 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -25,7 +25,7 @@ jobs: matrix: os: [macos-13] qt-version: [5.15.2, 6.7.1] - plugins: [false] + plugins: [true] fail-fast: false env: C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') && 'ON' || 'OFF' }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 926d5f907..b6b4c6014 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -26,7 +26,7 @@ jobs: matrix: os: [windows-latest] qt-version: [5.15.2, 6.7.1] - plugins: [false] + plugins: [true] skip-artifact: [false] skip-crashpad: [false] fail-fast: false diff --git a/.gitmodules b/.gitmodules index e58a5bbd4..e15a27575 100644 --- a/.gitmodules +++ b/.gitmodules @@ -44,3 +44,6 @@ [submodule "lib/expected-lite"] path = lib/expected-lite url = https://github.com/martinmoene/expected-lite +[submodule "lib/sol2"] + path = lib/sol2 + url = https://github.com/ThePhD/sol2.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fc9ad991..9ae647165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,7 @@ - Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607) - Dev: `GIFTimer` is no longer initialized in tests. (#5608) - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) +- Dev: Move plugins to Sol2. (#5622) - Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652) - Dev: Decoupled reply parsing from `MessageBuilder`. (#5660) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6fb323286..023135891 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -212,6 +212,8 @@ endif() if (CHATTERINO_PLUGINS) set(LUA_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/lib/lua/src") add_subdirectory(lib/lua) + + find_package(Sol2 REQUIRED) endif() if (BUILD_WITH_CRASHPAD) diff --git a/cmake/FindSol2.cmake b/cmake/FindSol2.cmake new file mode 100644 index 000000000..be64d000c --- /dev/null +++ b/cmake/FindSol2.cmake @@ -0,0 +1,21 @@ +include(FindPackageHandleStandardArgs) + +find_path(Sol2_INCLUDE_DIR sol/sol.hpp HINTS ${CMAKE_SOURCE_DIR}/lib/sol2/include) + +find_package_handle_standard_args(Sol2 DEFAULT_MSG Sol2_INCLUDE_DIR) + +if (Sol2_FOUND) + add_library(Sol2 INTERFACE IMPORTED) + set_target_properties(Sol2 PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Sol2_INCLUDE_DIR}" + ) + target_compile_definitions(Sol2 INTERFACE + SOL_ALL_SAFETIES_ON=1 + SOL_USING_CXX_LUA=1 + SOL_NO_NIL=0 + ) + target_link_libraries(Sol2 INTERFACE lua) + add_library(sol2::sol2 ALIAS Sol2) +endif () + +mark_as_advanced(Sol2_INCLUDE_DIR) diff --git a/docs/plugin-meta.lua b/docs/plugin-meta.lua index d4b1ac25b..5a86efca0 100644 --- a/docs/plugin-meta.lua +++ b/docs/plugin-meta.lua @@ -5,14 +5,23 @@ -- Add the folder this file is in to "Lua.workspace.library". c2 = {} ----@alias c2.LogLevel integer ----@type { Debug: c2.LogLevel, Info: c2.LogLevel, Warning: c2.LogLevel, Critical: c2.LogLevel } +---@alias c2.LogLevel.Debug "c2.LogLevel.Debug" +---@alias c2.LogLevel.Info "c2.LogLevel.Info" +---@alias c2.LogLevel.Warning "c2.LogLevel.Warning" +---@alias c2.LogLevel.Critical "c2.LogLevel.Critical" +---@alias c2.LogLevel c2.LogLevel.Debug|c2.LogLevel.Info|c2.LogLevel.Warning|c2.LogLevel.Critical +---@type { Debug: c2.LogLevel.Debug, Info: c2.LogLevel.Info, Warning: c2.LogLevel.Warning, Critical: c2.LogLevel.Critical } c2.LogLevel = {} ----@alias c2.EventType integer ----@type { CompletionRequested: c2.EventType } +-- Begin src/controllers/plugins/api/EventType.hpp + +---@alias c2.EventType.CompletionRequested "c2.EventType.CompletionRequested" +---@alias c2.EventType c2.EventType.CompletionRequested +---@type { CompletionRequested: c2.EventType.CompletionRequested } c2.EventType = {} +-- End src/controllers/plugins/api/EventType.hpp + ---@class CommandContext ---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`. ---@field channel c2.Channel The channel the command was executed in. @@ -29,19 +38,40 @@ c2.EventType = {} -- Begin src/common/Channel.hpp ----@alias c2.ChannelType integer ----@type { None: c2.ChannelType, Direct: c2.ChannelType, Twitch: c2.ChannelType, TwitchWhispers: c2.ChannelType, TwitchWatching: c2.ChannelType, TwitchMentions: c2.ChannelType, TwitchLive: c2.ChannelType, TwitchAutomod: c2.ChannelType, TwitchEnd: c2.ChannelType, Irc: c2.ChannelType, Misc: c2.ChannelType } +---@alias c2.ChannelType.None "c2.ChannelType.None" +---@alias c2.ChannelType.Direct "c2.ChannelType.Direct" +---@alias c2.ChannelType.Twitch "c2.ChannelType.Twitch" +---@alias c2.ChannelType.TwitchWhispers "c2.ChannelType.TwitchWhispers" +---@alias c2.ChannelType.TwitchWatching "c2.ChannelType.TwitchWatching" +---@alias c2.ChannelType.TwitchMentions "c2.ChannelType.TwitchMentions" +---@alias c2.ChannelType.TwitchLive "c2.ChannelType.TwitchLive" +---@alias c2.ChannelType.TwitchAutomod "c2.ChannelType.TwitchAutomod" +---@alias c2.ChannelType.TwitchEnd "c2.ChannelType.TwitchEnd" +---@alias c2.ChannelType.Misc "c2.ChannelType.Misc" +---@alias c2.ChannelType c2.ChannelType.None|c2.ChannelType.Direct|c2.ChannelType.Twitch|c2.ChannelType.TwitchWhispers|c2.ChannelType.TwitchWatching|c2.ChannelType.TwitchMentions|c2.ChannelType.TwitchLive|c2.ChannelType.TwitchAutomod|c2.ChannelType.TwitchEnd|c2.ChannelType.Misc +---@type { None: c2.ChannelType.None, Direct: c2.ChannelType.Direct, Twitch: c2.ChannelType.Twitch, TwitchWhispers: c2.ChannelType.TwitchWhispers, TwitchWatching: c2.ChannelType.TwitchWatching, TwitchMentions: c2.ChannelType.TwitchMentions, TwitchLive: c2.ChannelType.TwitchLive, TwitchAutomod: c2.ChannelType.TwitchAutomod, TwitchEnd: c2.ChannelType.TwitchEnd, Misc: c2.ChannelType.Misc } c2.ChannelType = {} -- End src/common/Channel.hpp -- Begin src/controllers/plugins/api/ChannelRef.hpp ----@alias c2.Platform integer ---- This enum describes a platform for the purpose of searching for a channel. ---- Currently only Twitch is supported because identifying IRC channels is tricky. ----@type { Twitch: c2.Platform } -c2.Platform = {} +-- Begin src/providers/twitch/TwitchChannel.hpp + +---@class StreamStatus +---@field live boolean +---@field viewer_count number +---@field title string Stream title or last stream title +---@field game_name string +---@field game_id string +---@field uptime number Seconds since the stream started. + +---@class RoomModes +---@field subscriber_only boolean +---@field unique_chat boolean You might know this as r9kbeta or robot9000. +---@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes + +-- End src/providers/twitch/TwitchChannel.hpp ---@class c2.Channel c2.Channel = {} @@ -72,7 +102,7 @@ function c2.Channel:get_display_name() end --- Note that this does not execute client-commands. --- ---@param message string ----@param execute_commands boolean Should commands be run on the text? +---@param execute_commands? boolean Should commands be run on the text? function c2.Channel:send_message(message, execute_commands) end --- Adds a system message client-side @@ -131,9 +161,8 @@ function c2.Channel:__tostring() end --- - /automod --- ---@param name string Which channel are you looking for? ----@param platform c2.Platform Where to search for the channel? ---@return c2.Channel? -function c2.Channel.by_name(name, platform) end +function c2.Channel.by_name(name) end --- Finds a channel by the Twitch user ID of its owner. --- @@ -141,21 +170,6 @@ function c2.Channel.by_name(name, platform) end ---@return c2.Channel? function c2.Channel.by_twitch_id(id) end ----@class RoomModes ----@field unique_chat boolean You might know this as r9kbeta or robot9000. ----@field subscriber_only boolean ----@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes ----@field follower_only number? Time in minutes you need to follow to chat or nil. ----@field slow_mode number? Time in seconds you need to wait before sending messages or nil. - ----@class StreamStatus ----@field live boolean ----@field viewer_count number ----@field uptime number Seconds since the stream started. ----@field title string Stream title or last stream title ----@field game_name string ----@field game_id string - -- End src/controllers/plugins/api/ChannelRef.hpp -- Begin src/controllers/plugins/api/HTTPResponse.hpp @@ -176,6 +190,9 @@ function HTTPResponse:status() end --- function HTTPResponse:error() end +---@return string +function HTTPResponse:__tostring() end + -- End src/controllers/plugins/api/HTTPResponse.hpp -- Begin src/controllers/plugins/api/HTTPRequest.hpp @@ -219,6 +236,9 @@ function HTTPRequest:set_header(name, value) end --- function HTTPRequest:execute() end +---@return string +function HTTPRequest:__tostring() end + --- Creates a new HTTPRequest --- ---@param method HTTPMethod Method to use @@ -230,8 +250,13 @@ function HTTPRequest.create(method, url) end -- Begin src/common/network/NetworkCommon.hpp ----@alias HTTPMethod integer ----@type { Get: HTTPMethod, Post: HTTPMethod, Put: HTTPMethod, Delete: HTTPMethod, Patch: HTTPMethod } +---@alias HTTPMethod.Get "HTTPMethod.Get" +---@alias HTTPMethod.Post "HTTPMethod.Post" +---@alias HTTPMethod.Put "HTTPMethod.Put" +---@alias HTTPMethod.Delete "HTTPMethod.Delete" +---@alias HTTPMethod.Patch "HTTPMethod.Patch" +---@alias HTTPMethod HTTPMethod.Get|HTTPMethod.Post|HTTPMethod.Put|HTTPMethod.Delete|HTTPMethod.Patch +---@type { Get: HTTPMethod.Get, Post: HTTPMethod.Post, Put: HTTPMethod.Put, Delete: HTTPMethod.Delete, Patch: HTTPMethod.Patch } HTTPMethod = {} -- End src/common/network/NetworkCommon.hpp @@ -245,7 +270,7 @@ function c2.register_command(name, handler) end --- Registers a callback to be invoked when completions for a term are requested. --- ----@param type "CompletionRequested" +---@param type c2.EventType.CompletionRequested ---@param func fun(event: CompletionEvent): CompletionList The callback to be invoked. function c2.register_callback(type, func) end diff --git a/docs/wip-plugins.md b/docs/wip-plugins.md index cd38fa18c..2fffb7429 100644 --- a/docs/wip-plugins.md +++ b/docs/wip-plugins.md @@ -171,7 +171,7 @@ function cmd_words(ctx) -- ctx contains: -- words - table of words supplied to the command including the trigger -- channel - the channel the command is being run in - channel:add_system_message("Words are: " .. table.concat(ctx.words, " ")) + ctx.channel:add_system_message("Words are: " .. table.concat(ctx.words, " ")) end c2.register_command("/words", cmd_words) @@ -183,7 +183,7 @@ Limitations/known issues: rebuilding the window content caused by reloading another plugin will solve this. - Spaces in command names aren't handled very well (https://github.com/Chatterino/chatterino2/issues/1517). -#### `register_callback("CompletionRequested", handler)` +#### `register_callback(c2.EventType.CompletionRequested, handler)` Registers a callback (`handler`) to process completions. The callback takes a single table with the following entries: @@ -207,7 +207,7 @@ function string.startswith(s, other) end c2.register_callback( - "CompletionRequested", + c2.EventType.CompletionRequested, function(event) if ("!join"):startswith(event.query) then ---@type CompletionList @@ -219,15 +219,6 @@ c2.register_callback( ) ``` -#### `Platform` enum - -This table describes platforms that can be accessed. Chatterino supports IRC -however plugins do not yet have explicit access to get IRC channels objects. -The values behind the names may change, do not count on them. It has the -following keys: - -- `Twitch` - #### `ChannelType` enum This table describes channel types Chatterino supports. The values behind the @@ -260,9 +251,9 @@ used on non-Twitch channels. Special channels while marked as is an actual Twitch chatroom use `Channel:get_type()` instead of `Channel:is_twitch_channel()`. -##### `Channel:by_name(name, platform)` +##### `Channel:by_name(name)` -Finds a channel given by `name` on `platform` (see [`Platform` enum](#Platform-enum)). Returns the channel or `nil` if not open. +Finds a channel given by `name`. Returns the channel or `nil` if not open. Some miscellaneous channels are marked as if they are specifically Twitch channels: @@ -275,7 +266,7 @@ Some miscellaneous channels are marked as if they are specifically Twitch channe Example: ```lua -local pajladas = c2.Channel.by_name("pajlada", c2.Platform.Twitch) +local pajladas = c2.Channel.by_name("pajlada") ``` ##### `Channel:by_twitch_id(id)` @@ -363,7 +354,7 @@ pajladas:add_system_message("Hello, world!") Returns `true` if the channel is a Twitch channel, that is its type name has the `Twitch` prefix. This returns `true` for special channels like Mentions. -You might want `Channel:get_type() == "Twitch"` if you want to use +You might want `Channel:get_type() == c2.ChannelType.Twitch` if you want to use Twitch-specific functions. ##### `Channel:get_twitch_id()` diff --git a/lib/lua/CMakeLists.txt b/lib/lua/CMakeLists.txt index cf2fad9bd..45824fdfa 100644 --- a/lib/lua/CMakeLists.txt +++ b/lib/lua/CMakeLists.txt @@ -1,48 +1,44 @@ project(lua CXX) #[====[ -Updating this list: -remove all listed files -go to line below, ^y2j4j$@" and then reindent the file names -/LUA_SRC -:r!ls lib/lua/src | grep '\.c' | grep -Ev 'lua\.c|onelua\.c' | sed 's#^#src/#' - +This list contains all .c files except lua.c and onelua.c +Use the following command from the repository root to get these file: +perl -e 'print s/^lib\/lua\///r . "\n" for grep { /\.c$/ && !/(lua|onelua)\.c$/ } glob "lib/lua/src/*.c"' #]====] set(LUA_SRC - "src/lapi.c" - "src/lauxlib.c" - "src/lbaselib.c" - "src/lcode.c" - "src/lcorolib.c" - "src/lctype.c" - "src/ldblib.c" - "src/ldebug.c" - "src/ldo.c" - "src/ldump.c" - "src/lfunc.c" - "src/lgc.c" - "src/linit.c" - "src/liolib.c" - "src/llex.c" - "src/lmathlib.c" - "src/lmem.c" - "src/loadlib.c" - "src/lobject.c" - "src/lopcodes.c" - "src/loslib.c" - "src/lparser.c" - "src/lstate.c" - "src/lstring.c" - "src/lstrlib.c" - "src/ltable.c" - "src/ltablib.c" - "src/ltests.c" - "src/ltm.c" - "src/lua.c" - "src/lundump.c" - "src/lutf8lib.c" - "src/lvm.c" - "src/lzio.c" + src/lapi.c + src/lauxlib.c + src/lbaselib.c + src/lcode.c + src/lcorolib.c + src/lctype.c + src/ldblib.c + src/ldebug.c + src/ldo.c + src/ldump.c + src/lfunc.c + src/lgc.c + src/linit.c + src/liolib.c + src/llex.c + src/lmathlib.c + src/lmem.c + src/loadlib.c + src/lobject.c + src/lopcodes.c + src/loslib.c + src/lparser.c + src/lstate.c + src/lstring.c + src/lstrlib.c + src/ltable.c + src/ltablib.c + src/ltests.c + src/ltm.c + src/lundump.c + src/lutf8lib.c + src/lvm.c + src/lzio.c ) add_library(lua STATIC ${LUA_SRC}) @@ -50,4 +46,14 @@ target_include_directories(lua PUBLIC ${LUA_INCLUDE_DIRS} ) -set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE C) +set_target_properties(${liblua} PROPERTIES + LANGUAGE CXX + LINKER_LANGUAGE CXX + CXX_STANDARD 98 + CXX_EXTENSIONS TRUE +) +target_compile_options(lua PRIVATE + -w # this makes clang shut up about c-as-c++ + $<$,$>:/EHsc> # enable exceptions in clang-cl +) +set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE CXX) diff --git a/lib/lua/src b/lib/lua/src index 0897c0a42..1ab3208a1 160000 --- a/lib/lua/src +++ b/lib/lua/src @@ -1 +1 @@ -Subproject commit 0897c0a4289ef3a8d45761266124613f364bef60 +Subproject commit 1ab3208a1fceb12fca8f24ba57d6e13c5bff15e3 diff --git a/lib/sol2 b/lib/sol2 new file mode 160000 index 000000000..2b0d2fe8b --- /dev/null +++ b/lib/sol2 @@ -0,0 +1 @@ +Subproject commit 2b0d2fe8ba0074e16b499940c4f3126b9c7d3471 diff --git a/scripts/make_luals_meta.py b/scripts/make_luals_meta.py index f9f44e7ed..b1420e780 100644 --- a/scripts/make_luals_meta.py +++ b/scripts/make_luals_meta.py @@ -196,7 +196,7 @@ def write_func(path: Path, line: int, comments: list[str], out: TextIOWrapper): if not comments[0].startswith("@"): out.write(f"--- {comments[0]}\n---\n") comments = comments[1:] - params = [] + params: list[str] = [] for comment in comments[:-1]: if not comment.startswith("@lua"): panic(path, line, f"Invalid function specification - got '{comment}'") @@ -209,7 +209,7 @@ def write_func(path: Path, line: int, comments: list[str], out: TextIOWrapper): panic(path, line, f"Invalid function exposure - got '{comments[-1]}'") name = comments[-1].split(" ", 1)[1] printmsg(path, line, f"function {name}") - lua_params = ", ".join(params) + lua_params = ", ".join(p.removesuffix("?") for p in params) out.write(f"function {name}({lua_params}) end\n\n") @@ -242,13 +242,21 @@ def read_file(path: Path, out: TextIOWrapper): ) name = header[0].split(" ", 1)[1] printmsg(path, reader.line_no(), f"enum {name}") - out.write(f"---@alias {name} integer\n") + variants = reader.read_enum_variants() + + vtypes = [] + for variant in variants: + vtype = f'{name}.{variant}' + vtypes.append(vtype) + out.write(f'---@alias {vtype} "{vtype}"\n') + + out.write(f"---@alias {name} {'|'.join(vtypes)}\n") if header_comment: out.write(f"--- {header_comment}\n") out.write("---@type { ") out.write( ", ".join( - [f"{variant}: {name}" for variant in reader.read_enum_variants()] + [f"{variant}: {typ}" for variant, typ in zip(variants,vtypes)] ) ) out.write(" }\n") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4c24ef572..9d7134b97 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -225,24 +225,28 @@ set(SOURCE_FILES controllers/pings/MutedChannelModel.cpp controllers/pings/MutedChannelModel.hpp + controllers/plugins/api/ChannelRef.cpp controllers/plugins/api/ChannelRef.hpp - controllers/plugins/api/IOWrapper.cpp - controllers/plugins/api/IOWrapper.hpp + controllers/plugins/api/EventType.hpp controllers/plugins/api/HTTPRequest.cpp controllers/plugins/api/HTTPRequest.hpp controllers/plugins/api/HTTPResponse.cpp controllers/plugins/api/HTTPResponse.hpp + controllers/plugins/api/IOWrapper.cpp + controllers/plugins/api/IOWrapper.hpp controllers/plugins/LuaAPI.cpp controllers/plugins/LuaAPI.hpp - controllers/plugins/PluginPermission.cpp - controllers/plugins/PluginPermission.hpp - controllers/plugins/Plugin.cpp - controllers/plugins/Plugin.hpp - controllers/plugins/PluginController.hpp - controllers/plugins/PluginController.cpp controllers/plugins/LuaUtilities.cpp controllers/plugins/LuaUtilities.hpp + controllers/plugins/PluginController.cpp + controllers/plugins/PluginController.hpp + controllers/plugins/Plugin.cpp + controllers/plugins/Plugin.hpp + controllers/plugins/PluginPermission.cpp + controllers/plugins/PluginPermission.hpp + controllers/plugins/SolTypes.cpp + controllers/plugins/SolTypes.hpp controllers/sound/ISoundController.hpp controllers/sound/MiniaudioBackend.cpp @@ -791,7 +795,7 @@ target_link_libraries(${LIBRARY_PROJECT} $<$:Wtsapi32> ) if (CHATTERINO_PLUGINS) - target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua) + target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua sol2::sol2) endif() if (BUILD_WITH_QTKEYCHAIN) diff --git a/src/PrecompiledHeader.hpp b/src/PrecompiledHeader.hpp index d7c289bca..a12265950 100644 --- a/src/PrecompiledHeader.hpp +++ b/src/PrecompiledHeader.hpp @@ -129,6 +129,10 @@ # include # include +# ifdef CHATTERINO_HAVE_PLUGINS +# include +# endif + # ifndef UNUSED # define UNUSED(x) (void)(x) # endif diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 554327622..ac90573ff 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -165,30 +165,3 @@ private: }; } // namespace chatterino - -template <> -constexpr magic_enum::customize::customize_t - magic_enum::customize::enum_name( - chatterino::Channel::Type value) noexcept -{ - using Type = chatterino::Channel::Type; - switch (value) - { - case Type::Twitch: - return "twitch"; - case Type::TwitchWhispers: - return "whispers"; - case Type::TwitchWatching: - return "watching"; - case Type::TwitchMentions: - return "mentions"; - case Type::TwitchLive: - return "live"; - case Type::TwitchAutomod: - return "automod"; - case Type::Misc: - return "misc"; - default: - return default_tag; - } -} diff --git a/src/controllers/plugins/LuaAPI.cpp b/src/controllers/plugins/LuaAPI.cpp index 03d11750a..4f73c51ee 100644 --- a/src/controllers/plugins/LuaAPI.cpp +++ b/src/controllers/plugins/LuaAPI.cpp @@ -3,34 +3,45 @@ # include "Application.hpp" # include "common/QLogging.hpp" -# include "controllers/commands/CommandController.hpp" # include "controllers/plugins/LuaUtilities.hpp" # include "controllers/plugins/PluginController.hpp" -# include "messages/MessageBuilder.hpp" -# include "providers/twitch/TwitchIrcServer.hpp" +# include "controllers/plugins/SolTypes.hpp" // for lua operations on QString{,List} for CompletionList -extern "C" { # include # include # include -} # include +# include # include # include # include +# include +# include +# include +# include +# include +# include +# include +# include + +# include +# include +# include namespace { using namespace chatterino; -void logHelper(lua_State *L, Plugin *pl, QDebug stream, int argc) +void logHelper(lua_State *L, Plugin *pl, QDebug stream, + const sol::variadic_args &args) { stream.noquote(); stream << "[" + pl->id + ":" + pl->meta.name + "]"; - for (int i = 1; i <= argc; i++) + for (const auto &arg : args) { - stream << lua::toString(L, i); + stream << lua::toString(L, arg.stack_index()); + // Remove this from our stack + lua_pop(L, 1); } - lua_pop(L, argc); } QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl) @@ -63,195 +74,92 @@ QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl) // luaL_error is a c-style vararg function, this makes clang-tidy not dislike it so much namespace chatterino::lua::api { -int c2_register_command(lua_State *L) +CompletionList::CompletionList(const sol::table &table) + : values(table.get("values")) + , hideOthers(table["hide_others"]) { - auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); - if (pl == nullptr) - { - luaL_error(L, "internal error: no plugin"); - return 0; - } - - QString name; - if (!lua::peek(L, &name, 1)) - { - luaL_error(L, "cannot get command name (1st arg of register_command, " - "expected a string)"); - return 0; - } - if (lua_isnoneornil(L, 2)) - { - luaL_error(L, "missing argument for register_command: function " - "\"pointer\""); - return 0; - } - - auto callbackSavedName = QString("c2commandcb-%1").arg(name); - lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.toStdString().c_str()); - auto ok = pl->registerCommand(name, callbackSavedName); - - // delete both name and callback - lua_pop(L, 2); - - lua::push(L, ok); - return 1; } -int c2_register_callback(lua_State *L) +sol::table toTable(lua_State *L, const CompletionEvent &ev) { - auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); - if (pl == nullptr) - { - luaL_error(L, "internal error: no plugin"); - return 0; - } - EventType evtType{}; - if (!lua::peek(L, &evtType, 1)) - { - luaL_error(L, "cannot get event name (1st arg of register_callback, " - "expected a string)"); - return 0; - } - if (lua_isnoneornil(L, 2)) - { - luaL_error(L, "missing argument for register_callback: function " - "\"pointer\""); - return 0; - } - - auto typeName = magic_enum::enum_name(evtType); - std::string callbackSavedName; - callbackSavedName.reserve(5 + typeName.size()); - callbackSavedName += "c2cb-"; - callbackSavedName += typeName; - lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.c_str()); - - lua_pop(L, 2); - - return 0; + return sol::state_view(L).create_table_with( + "query", ev.query, // + "full_text_content", ev.full_text_content, // + "cursor_position", ev.cursor_position, // + "is_first_word", ev.is_first_word // + ); } -int c2_log(lua_State *L) +void c2_register_callback(ThisPluginState L, EventType evtType, + sol::protected_function callback) { - auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); - if (pl == nullptr) - { - luaL_error(L, "c2_log: internal error: no plugin?"); - return 0; - } - auto logc = lua_gettop(L) - 1; - // This is almost the expansion of qCDebug() macro, actual thing is wrapped in a for loop - LogLevel lvl{}; - if (!lua::pop(L, &lvl, 1)) - { - luaL_error(L, "Invalid log level, use one from c2.LogLevel."); - return 0; - } - QDebug stream = qdebugStreamForLogLevel(lvl); - logHelper(L, pl, stream, logc); - return 0; + L.plugin()->callbacks[evtType] = std::move(callback); } -int c2_later(lua_State *L) +void c2_log(ThisPluginState L, LogLevel lvl, sol::variadic_args args) { - auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); - if (pl == nullptr) + lua::StackGuard guard(L); { - return luaL_error(L, "c2.later: internal error: no plugin?"); - } - if (lua_gettop(L) != 2) - { - return luaL_error( - L, "c2.later expects two arguments (a callback that takes no " - "arguments and returns nothing and a number the time in " - "milliseconds to wait)\n"); - } - int time{}; - if (!lua::pop(L, &time)) - { - return luaL_error(L, "cannot get time (2nd arg of c2.later, " - "expected a number)"); + QDebug stream = qdebugStreamForLogLevel(lvl); + logHelper(L, L.plugin(), stream, args); } +} - if (!lua_isfunction(L, lua_gettop(L))) +void c2_later(ThisPluginState L, sol::protected_function callback, int time) +{ + if (time <= 0) { - return luaL_error(L, "cannot get callback (1st arg of c2.later, " - "expected a function)"); + throw std::runtime_error( + "c2.later time must be strictly greater than zero."); } + sol::state_view lua(L); auto *timer = new QTimer(); timer->setInterval(time); - auto id = pl->addTimeout(timer); + timer->setSingleShot(true); + auto id = L.plugin()->addTimeout(timer); auto name = QString("timeout_%1").arg(id); - auto *coro = lua_newthread(L); - QObject::connect(timer, &QTimer::timeout, [pl, coro, name, timer]() { - timer->deleteLater(); - pl->removeTimeout(timer); - int nres{}; - lua_resume(coro, nullptr, 0, &nres); + sol::state_view main = sol::main_thread(L); - lua_pushnil(coro); - lua_setfield(coro, LUA_REGISTRYINDEX, name.toStdString().c_str()); - if (lua_gettop(coro) != 0) - { - stackDump(coro, - pl->id + - ": timer returned a value, this shouldn't happen " - "and is probably a plugin bug"); - } - }); - stackDump(L, "before setfield"); - lua_setfield(L, LUA_REGISTRYINDEX, name.toStdString().c_str()); - lua_xmove(L, coro, 1); // move function to thread + sol::thread thread = sol::thread::create(main); + sol::protected_function cb(thread.state(), callback); + main.registry()[name.toStdString()] = thread; + + QObject::connect( + timer, &QTimer::timeout, + [pl = L.plugin(), name, timer, cb, thread, main]() { + timer->deleteLater(); + pl->removeTimeout(timer); + sol::protected_function_result res = cb(); + + if (res.return_count() != 0) + { + stackDump(thread.lua_state(), + pl->id + + ": timer returned a value, this shouldn't happen " + "and is probably a plugin bug"); + } + main.registry()[name.toStdString()] = sol::nil; + }); timer->start(); - - return 0; } -int g_load(lua_State *L) +// TODO: Add tests for this once we run tests in debug mode +sol::variadic_results g_load(ThisPluginState s, sol::object data) { # ifdef NDEBUG - luaL_error(L, "load() is only usable in debug mode"); - return 0; + (void)data; + (void)s; + throw std::runtime_error("load() is only usable in debug mode"); # else - auto countArgs = lua_gettop(L); - QByteArray data; - if (lua::peek(L, &data, 1)) - { - auto *utf8 = QTextCodec::codecForName("UTF-8"); - QTextCodec::ConverterState state; - utf8->toUnicode(data.constData(), data.size(), &state); - if (state.invalidChars != 0) - { - luaL_error(L, "invalid utf-8 in load() is not allowed"); - return 0; - } - } - else - { - luaL_error(L, "using reader function in load() is not allowed"); - return 0; - } - for (int i = 0; i < countArgs; i++) - { - lua_seti(L, LUA_REGISTRYINDEX, i); - } - - // fetch load and call it - lua_getfield(L, LUA_REGISTRYINDEX, "real_load"); - - for (int i = 0; i < countArgs; i++) - { - lua_geti(L, LUA_REGISTRYINDEX, i); - lua_pushnil(L); - lua_seti(L, LUA_REGISTRYINDEX, i); - } - - lua_call(L, countArgs, LUA_MULTRET); - - return lua_gettop(L); + // If you're modifying this PLEASE verify it works, Sol is very annoying about serialization + // - Mm2PL + sol::state_view lua(s); + auto load = lua.registry()["real_load"]; + sol::protected_function_result ret = load(data, "=(load)", "t"); + return ret; # endif } @@ -320,7 +228,7 @@ int searcherAbsolute(lua_State *L) int searcherRelative(lua_State *L) { lua_Debug dbg; - lua_getstack(L, 1, &dbg); + lua_getstack(L, 2, &dbg); lua_getinfo(L, "S", &dbg); auto currentFile = QString::fromUtf8(dbg.source, dbg.srclen); if (currentFile.startsWith("@")) @@ -346,22 +254,14 @@ int searcherRelative(lua_State *L) return loadfile(L, filename); } -int g_print(lua_State *L) +void g_print(ThisPluginState L, sol::variadic_args args) { - auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); - if (pl == nullptr) - { - luaL_error(L, "c2_print: internal error: no plugin?"); - return 0; - } - auto argc = lua_gettop(L); // This is almost the expansion of qCDebug() macro, actual thing is wrapped in a for loop auto stream = (QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC, chatterinoLua().categoryName()) .debug()); - logHelper(L, pl, stream, argc); - return 0; + logHelper(L, L.plugin(), stream, args); } } // namespace chatterino::lua::api diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index 904c6daa4..bd83dee5a 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -1,17 +1,17 @@ #pragma once #ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/api/ChannelRef.hpp" +# include "controllers/plugins/Plugin.hpp" +# include "controllers/plugins/SolTypes.hpp" -extern "C" { # include -} -# include "controllers/plugins/LuaUtilities.hpp" - +# include # include +# include # include # include -# include struct lua_State; namespace chatterino::lua::api { @@ -30,11 +30,8 @@ namespace chatterino::lua::api { enum class LogLevel { Debug, Info, Warning, Critical }; /** - * @exposeenum c2.EventType + * @includefile controllers/plugins/api/EventType.hpp */ -enum class EventType { - CompletionRequested, -}; /** * @lua@class CommandContext @@ -46,10 +43,12 @@ enum class EventType { * @lua@class CompletionList */ struct CompletionList { + CompletionList(const sol::table &); + /** * @lua@field values string[] The completions */ - std::vector values{}; + QStringList values; /** * @lua@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored. @@ -79,6 +78,8 @@ struct CompletionEvent { bool is_first_word{}; }; +sol::table toTable(lua_State *L, const CompletionEvent &ev); + /** * @includefile common/Channel.hpp * @includefile controllers/plugins/api/ChannelRef.hpp @@ -95,16 +96,16 @@ struct CompletionEvent { * @lua@return boolean ok Returns `true` if everything went ok, `false` if a command with this name exists. * @exposed c2.register_command */ -int c2_register_command(lua_State *L); /** * Registers a callback to be invoked when completions for a term are requested. * - * @lua@param type "CompletionRequested" + * @lua@param type c2.EventType.CompletionRequested * @lua@param func fun(event: CompletionEvent): CompletionList The callback to be invoked. * @exposed c2.register_callback */ -int c2_register_callback(lua_State *L); +void c2_register_callback(ThisPluginState L, EventType evtType, + sol::protected_function callback); /** * Writes a message to the Chatterino log. @@ -113,7 +114,7 @@ int c2_register_callback(lua_State *L); * @lua@param ... any Values to log. Should be convertible to a string with `tostring()`. * @exposed c2.log */ -int c2_log(lua_State *L); +void c2_log(ThisPluginState L, LogLevel lvl, sol::variadic_args args); /** * Calls callback around msec milliseconds later. Does not freeze Chatterino. @@ -122,11 +123,11 @@ int c2_log(lua_State *L); * @lua@param msec number How long to wait. * @exposed c2.later */ -int c2_later(lua_State *L); +void c2_later(ThisPluginState L, sol::protected_function callback, int time); // These ones are global -int g_load(lua_State *L); -int g_print(lua_State *L); +sol::variadic_results g_load(ThisPluginState s, sol::object data); +void g_print(ThisPluginState L, sol::variadic_args args); // NOLINTEND(readability-identifier-naming) // This is for require() exposed as an element of package.searchers diff --git a/src/controllers/plugins/LuaUtilities.cpp b/src/controllers/plugins/LuaUtilities.cpp index 64af18c01..58f648f0a 100644 --- a/src/controllers/plugins/LuaUtilities.cpp +++ b/src/controllers/plugins/LuaUtilities.cpp @@ -1,16 +1,10 @@ #ifdef CHATTERINO_HAVE_PLUGINS # include "controllers/plugins/LuaUtilities.hpp" -# include "common/Channel.hpp" # include "common/QLogging.hpp" -# include "controllers/commands/CommandContext.hpp" -# include "controllers/plugins/api/ChannelRef.hpp" -# include "controllers/plugins/LuaAPI.hpp" -extern "C" { # include # include -} # include # include @@ -79,9 +73,6 @@ QString humanErrorText(lua_State *L, int errCode) case LUA_ERRFILE: errName = "(file error)"; break; - case ERROR_BAD_PEEK: - errName = "(unable to convert value to c++)"; - break; default: errName = "(unknown error type)"; } @@ -93,18 +84,6 @@ QString humanErrorText(lua_State *L, int errCode) return errName; } -StackIdx pushEmptyArray(lua_State *L, int countArray) -{ - lua_createtable(L, countArray, 0); - return lua_gettop(L); -} - -StackIdx pushEmptyTable(lua_State *L, int countProperties) -{ - lua_createtable(L, 0, countProperties); - return lua_gettop(L); -} - StackIdx push(lua_State *L, const QString &str) { return lua::push(L, str.toStdString()); @@ -116,82 +95,6 @@ StackIdx push(lua_State *L, const std::string &str) return lua_gettop(L); } -StackIdx push(lua_State *L, const CommandContext &ctx) -{ - StackGuard guard(L, 1); - auto outIdx = pushEmptyTable(L, 2); - - push(L, ctx.words); - lua_setfield(L, outIdx, "words"); - - push(L, ctx.channel); - lua_setfield(L, outIdx, "channel"); - - return outIdx; -} - -StackIdx push(lua_State *L, const bool &b) -{ - lua_pushboolean(L, int(b)); - return lua_gettop(L); -} - -StackIdx push(lua_State *L, const int &b) -{ - lua_pushinteger(L, b); - return lua_gettop(L); -} - -StackIdx push(lua_State *L, const api::CompletionEvent &ev) -{ - auto idx = pushEmptyTable(L, 4); -# define PUSH(field) \ - lua::push(L, ev.field); \ - lua_setfield(L, idx, #field) - PUSH(query); - PUSH(full_text_content); - PUSH(cursor_position); - PUSH(is_first_word); -# undef PUSH - return idx; -} - -bool peek(lua_State *L, int *out, StackIdx idx) -{ - StackGuard guard(L); - if (lua_isnumber(L, idx) == 0) - { - return false; - } - - *out = lua_tointeger(L, idx); - return true; -} - -bool peek(lua_State *L, bool *out, StackIdx idx) -{ - StackGuard guard(L); - if (!lua_isboolean(L, idx)) - { - return false; - } - - *out = bool(lua_toboolean(L, idx)); - return true; -} - -bool peek(lua_State *L, double *out, StackIdx idx) -{ - StackGuard guard(L); - int ok{0}; - auto v = lua_tonumberx(L, idx, &ok); - if (ok != 0) - { - *out = v; - } - return ok != 0; -} - bool peek(lua_State *L, QString *out, StackIdx idx) { StackGuard guard(L); @@ -209,57 +112,6 @@ bool peek(lua_State *L, QString *out, StackIdx idx) return true; } -bool peek(lua_State *L, QByteArray *out, StackIdx idx) -{ - StackGuard guard(L); - size_t len{0}; - const char *str = lua_tolstring(L, idx, &len); - if (str == nullptr) - { - return false; - } - if (len >= INT_MAX) - { - assert(false && "string longer than INT_MAX, shit's fucked, yo"); - } - *out = QByteArray(str, int(len)); - return true; -} - -bool peek(lua_State *L, std::string *out, StackIdx idx) -{ - StackGuard guard(L); - size_t len{0}; - const char *str = lua_tolstring(L, idx, &len); - if (str == nullptr) - { - return false; - } - if (len >= INT_MAX) - { - assert(false && "string longer than INT_MAX, shit's fucked, yo"); - } - *out = std::string(str, len); - return true; -} - -bool peek(lua_State *L, api::CompletionList *out, StackIdx idx) -{ - StackGuard guard(L); - int typ = lua_getfield(L, idx, "values"); - if (typ != LUA_TTABLE) - { - lua_pop(L, 1); - return false; - } - if (!lua::pop(L, &out->values, -1)) - { - return false; - } - lua_getfield(L, idx, "hide_others"); - return lua::pop(L, &out->hideOthers); -} - QString toString(lua_State *L, StackIdx idx) { size_t len{}; diff --git a/src/controllers/plugins/LuaUtilities.hpp b/src/controllers/plugins/LuaUtilities.hpp index 5443a751f..0f7bdc53f 100644 --- a/src/controllers/plugins/LuaUtilities.hpp +++ b/src/controllers/plugins/LuaUtilities.hpp @@ -2,37 +2,20 @@ #ifdef CHATTERINO_HAVE_PLUGINS -# include "common/QLogging.hpp" - -extern "C" { # include # include -} # include # include +# include # include -# include # include # include # include -# include -# include struct lua_State; -class QJsonObject; -namespace chatterino { -struct CommandContext; -} // namespace chatterino namespace chatterino::lua { -namespace api { - struct CompletionList; - struct CompletionEvent; -} // namespace api - -constexpr int ERROR_BAD_PEEK = LUA_OK - 1; - /** * @brief Dumps the Lua stack into qCDebug(chatterinoLua) * @@ -40,6 +23,9 @@ constexpr int ERROR_BAD_PEEK = LUA_OK - 1; */ void stackDump(lua_State *L, const QString &tag); +// This is for calling stackDump out of gdb as it's not easy to create a QString there +const QString GDB_DUMMY = "GDB_DUMMY"; + /** * @brief Converts a lua error code and potentially string on top of the stack into a human readable message */ @@ -50,33 +36,11 @@ QString humanErrorText(lua_State *L, int errCode); */ using StackIdx = int; -/** - * @brief Creates a table with countArray array properties on the Lua stack - * @return stack index of the newly created table - */ -StackIdx pushEmptyArray(lua_State *L, int countArray); - -/** - * @brief Creates a table with countProperties named properties on the Lua stack - * @return stack index of the newly created table - */ -StackIdx pushEmptyTable(lua_State *L, int countProperties); - -StackIdx push(lua_State *L, const CommandContext &ctx); StackIdx push(lua_State *L, const QString &str); StackIdx push(lua_State *L, const std::string &str); -StackIdx push(lua_State *L, const bool &b); -StackIdx push(lua_State *L, const int &b); -StackIdx push(lua_State *L, const api::CompletionEvent &ev); // returns OK? -bool peek(lua_State *L, int *out, StackIdx idx = -1); -bool peek(lua_State *L, bool *out, StackIdx idx = -1); -bool peek(lua_State *L, double *out, StackIdx idx = -1); bool peek(lua_State *L, QString *out, StackIdx idx = -1); -bool peek(lua_State *L, QByteArray *out, StackIdx idx = -1); -bool peek(lua_State *L, std::string *out, StackIdx idx = -1); -bool peek(lua_State *L, api::CompletionList *out, StackIdx idx = -1); /** * @brief Converts Lua object at stack index idx to a string. @@ -140,246 +104,29 @@ public: } }; -/// TEMPLATES - -template -StackIdx push(lua_State *L, std::optional val) -{ - if (val.has_value()) - { - return lua::push(L, *val); - } - lua_pushnil(L); - return lua_gettop(L); -} - -template -bool peek(lua_State *L, std::optional *out, StackIdx idx = -1) -{ - if (lua_isnil(L, idx)) - { - *out = std::nullopt; - return true; - } - - *out = T(); - return peek(L, out->operator->(), idx); -} - -template -bool peek(lua_State *L, std::vector *vec, StackIdx idx = -1) -{ - StackGuard guard(L); - - if (!lua_istable(L, idx)) - { - lua::stackDump(L, "!table"); - qCDebug(chatterinoLua) - << "value is not a table, type is" << lua_type(L, idx); - return false; - } - auto len = lua_rawlen(L, idx); - if (len == 0) - { - qCDebug(chatterinoLua) << "value has 0 length"; - return true; - } - if (len > 1'000'000) - { - qCDebug(chatterinoLua) << "value is too long"; - return false; - } - // count like lua - for (int i = 1; i <= len; i++) - { - lua_geti(L, idx, i); - std::optional obj; - if (!lua::peek(L, &obj)) - { - //lua_seti(L, LUA_REGISTRYINDEX, 1); // lazy - qCDebug(chatterinoLua) - << "Failed to convert lua object into c++: at array index " << i - << ":"; - stackDump(L, "bad conversion into string"); - return false; - } - lua_pop(L, 1); - vec->push_back(obj.value()); - } - return true; -} - -/** - * @brief Converts object at stack index idx to enum given by template parameter T - */ -template , bool>::type = true> -bool peek(lua_State *L, T *out, StackIdx idx = -1) -{ - std::string tmp; - if (!lua::peek(L, &tmp, idx)) - { - return false; - } - std::optional opt = magic_enum::enum_cast(tmp); - if (opt.has_value()) - { - *out = opt.value(); - return true; - } - - return false; -} - -/** - * @brief Converts a vector to Lua and pushes it onto the stack. - * - * Needs StackIdx push(lua_State*, T); to work. - * - * @return Stack index of newly created table. - */ -template -StackIdx push(lua_State *L, std::vector vec) -{ - auto out = pushEmptyArray(L, vec.size()); - int i = 1; - for (const auto &el : vec) - { - push(L, el); - lua_seti(L, out, i); - i += 1; - } - return out; -} - -/** - * @brief Converts a QList to Lua and pushes it onto the stack. - * - * Needs StackIdx push(lua_State*, T); to work. - * - * @return Stack index of newly created table. - */ -template -StackIdx push(lua_State *L, QList vec) -{ - auto out = pushEmptyArray(L, vec.size()); - int i = 1; - for (const auto &el : vec) - { - push(L, el); - lua_seti(L, out, i); - i += 1; - } - return out; -} - -/** - * @brief Converts an enum given by T to Lua (into a string) and pushes it onto the stack. - * - * @return Stack index of newly created string. - */ -template , bool> = true> -StackIdx push(lua_State *L, T inp) -{ - std::string_view name = magic_enum::enum_name(inp); - return lua::push(L, std::string(name)); -} - -/** - * @brief Converts a Lua object into c++ and removes it from the stack. - * If peek fails, the object is still removed from the stack. - * - * Relies on bool peek(lua_State*, T*, StackIdx) existing. - */ -template -bool pop(lua_State *L, T *out, StackIdx idx = -1) -{ - StackGuard guard(L, -1); - auto ok = peek(L, out, idx); - if (idx < 0) - { - idx = lua_gettop(L) + idx + 1; - } - lua_remove(L, idx); - return ok; -} - /** * @brief Creates a table mapping enum names to unique values. * * Values in this table may change. * - * @returns stack index of newly created table + * @returns Sol reference to the table */ template -StackIdx pushEnumTable(lua_State *L) + requires std::is_enum_v +sol::table createEnumTable(sol::state_view &lua) { - // std::array - auto values = magic_enum::enum_values(); - StackIdx out = lua::pushEmptyTable(L, values.size()); + constexpr auto values = magic_enum::enum_values(); + auto out = lua.create_table(0, values.size()); for (const T v : values) { std::string_view name = magic_enum::enum_name(v); std::string str(name); - lua::push(L, str); - lua_setfield(L, out, str.c_str()); + out.raw_set(str, v); } return out; } -// Represents a Lua function on the stack -template -class CallbackFunction -{ - StackIdx stackIdx_; - lua_State *L; - -public: - CallbackFunction(lua_State *L, StackIdx stackIdx) - : stackIdx_(stackIdx) - , L(L) - { - } - - // this type owns the stackidx, it must not be trivially copiable - CallbackFunction operator=(CallbackFunction &) = delete; - CallbackFunction(CallbackFunction &) = delete; - - // Permit only move - CallbackFunction &operator=(CallbackFunction &&) = default; - CallbackFunction(CallbackFunction &&) = default; - - ~CallbackFunction() - { - lua_remove(L, this->stackIdx_); - } - - std::variant operator()(Args... arguments) - { - lua_pushvalue(this->L, this->stackIdx_); - ( // apparently this calls lua::push() for every Arg - [this, &arguments] { - lua::push(this->L, arguments); - }(), - ...); - - int res = lua_pcall(L, sizeof...(Args), 1, 0); - if (res != LUA_OK) - { - qCDebug(chatterinoLua) << "error is: " << res; - return {res}; - } - - ReturnType val; - if (!lua::pop(L, &val)) - { - return {ERROR_BAD_PEEK}; - } - return {val}; - } -}; - } // namespace chatterino::lua #endif diff --git a/src/controllers/plugins/Plugin.cpp b/src/controllers/plugins/Plugin.cpp index dcf7357e8..739fa6372 100644 --- a/src/controllers/plugins/Plugin.cpp +++ b/src/controllers/plugins/Plugin.cpp @@ -7,14 +7,13 @@ # include "controllers/plugins/PluginPermission.hpp" # include "util/QMagicEnum.hpp" -extern "C" { # include -} # include # include # include # include # include +# include # include # include @@ -190,7 +189,8 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } } -bool Plugin::registerCommand(const QString &name, const QString &functionName) +bool Plugin::registerCommand(const QString &name, + sol::protected_function function) { if (this->ownedCommands.find(name) != this->ownedCommands.end()) { @@ -202,7 +202,7 @@ bool Plugin::registerCommand(const QString &name, const QString &functionName) { return false; } - this->ownedCommands.insert({name, functionName}); + this->ownedCommands.emplace(name, std::move(function)); return true; } @@ -223,14 +223,24 @@ Plugin::~Plugin() QObject::disconnect(timer, nullptr, nullptr, nullptr); timer->deleteLater(); } + this->httpRequests.clear(); qCDebug(chatterinoLua) << "Destroyed" << this->activeTimeouts.size() << "timers for plugin" << this->id << "while destroying the object"; this->activeTimeouts.clear(); if (this->state_ != nullptr) { + // clearing this after the state is gone is not safe to do + this->ownedCommands.clear(); + this->callbacks.clear(); lua_close(this->state_); } + assert(this->ownedCommands.empty() && + "This must be empty or destructor of sol::protected_function would " + "explode malloc structures later"); + assert(this->callbacks.empty() && + "This must be empty or destructor of sol::protected_function would " + "explode malloc structures later"); } int Plugin::addTimeout(QTimer *timer) { diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp index f8375247f..a65329468 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -2,8 +2,8 @@ #ifdef CHATTERINO_HAVE_PLUGINS # include "Application.hpp" -# include "common/network/NetworkCommon.hpp" -# include "controllers/plugins/LuaAPI.hpp" +# include "controllers/plugins/api/EventType.hpp" +# include "controllers/plugins/api/HTTPRequest.hpp" # include "controllers/plugins/LuaUtilities.hpp" # include "controllers/plugins/PluginPermission.hpp" @@ -11,7 +11,10 @@ # include # include # include +# include +# include +# include # include # include # include @@ -56,6 +59,8 @@ struct PluginMeta { } explicit PluginMeta(const QJsonObject &obj); + // This is for tests + PluginMeta() = default; }; class Plugin @@ -75,13 +80,18 @@ public: ~Plugin(); + Plugin(const Plugin &) = delete; + Plugin(Plugin &&) = delete; + Plugin &operator=(const Plugin &) = delete; + Plugin &operator=(Plugin &&) = delete; + /** * @brief Perform all necessary tasks to bind a command name to this plugin * @param name name of the command to create - * @param functionName name of the function that should be called when the command is executed + * @param function the function that should be called when the command is executed * @return true if addition succeeded, false otherwise (for example because the command name is already taken) */ - bool registerCommand(const QString &name, const QString &functionName); + bool registerCommand(const QString &name, sol::protected_function function); /** * @brief Get names of all commands belonging to this plugin @@ -98,35 +108,19 @@ public: return this->loadDirectory_.absoluteFilePath("data"); } - // Note: The CallbackFunction object's destructor will remove the function from the lua stack - using LuaCompletionCallback = - lua::CallbackFunction; - std::optional getCompletionCallback() + std::optional getCompletionCallback() { if (this->state_ == nullptr || !this->error_.isNull()) { return {}; } - // this uses magic enum to help automatic tooling find usages - auto typeName = - magic_enum::enum_name(lua::api::EventType::CompletionRequested); - std::string cbName; - cbName.reserve(5 + typeName.size()); - cbName += "c2cb-"; - cbName += typeName; - auto typ = - lua_getfield(this->state_, LUA_REGISTRYINDEX, cbName.c_str()); - if (typ != LUA_TFUNCTION) + auto it = + this->callbacks.find(lua::api::EventType::CompletionRequested); + if (it == this->callbacks.end()) { - lua_pop(this->state_, 1); return {}; } - - // move - return std::make_optional>( - this->state_, lua_gettop(this->state_)); + return it->second; } /** @@ -143,18 +137,25 @@ public: bool hasFSPermissionFor(bool write, const QString &path); bool hasHTTPPermissionFor(const QUrl &url); + std::map callbacks; + + // In-flight HTTP Requests + // This is a lifetime hack to ensure they get deleted with the plugin. This relies on the Plugin getting deleted on reload! + std::vector> httpRequests; + private: QDir loadDirectory_; lua_State *state_; QString error_; - // maps command name -> function name - std::unordered_map ownedCommands; + // maps command name -> function + std::unordered_map ownedCommands; std::vector activeTimeouts; int lastTimerId = 0; friend class PluginController; + friend class PluginControllerAccess; // this is for tests }; } // namespace chatterino #endif diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 5ca77ed40..1a2bc3a10 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -13,16 +13,20 @@ # include "controllers/plugins/api/IOWrapper.hpp" # include "controllers/plugins/LuaAPI.hpp" # include "controllers/plugins/LuaUtilities.hpp" +# include "controllers/plugins/SolTypes.hpp" # include "messages/MessageBuilder.hpp" # include "singletons/Paths.hpp" # include "singletons/Settings.hpp" -extern "C" { # include # include # include -} # include +# include +# include +# include +# include +# include # include # include @@ -113,10 +117,11 @@ bool PluginController::tryLoadFromDir(const QDir &pluginDir) return true; } -void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta, - const QDir &pluginDir) +void PluginController::openLibrariesFor(Plugin *plugin) { + auto *L = plugin->state_; lua::StackGuard guard(L); + sol::state_view lua(L); // Stuff to change, remove or hide behind a permission system: static const std::vector loadedlibs = { luaL_Reg{LUA_GNAME, luaopen_base}, @@ -124,8 +129,6 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta, luaL_Reg{LUA_COLIBNAME, luaopen_coroutine}, luaL_Reg{LUA_TABLIBNAME, luaopen_table}, - // luaL_Reg{LUA_IOLIBNAME, luaopen_io}, - // - explicit fs access, needs wrapper with permissions, no usage ideas yet // luaL_Reg{LUA_OSLIBNAME, luaopen_os}, // - fs access // - environ access @@ -147,155 +150,100 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta, luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, int(false)); lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME); - // NOLINTNEXTLINE(*-avoid-c-arrays) - static const luaL_Reg c2Lib[] = { - {"register_command", lua::api::c2_register_command}, - {"register_callback", lua::api::c2_register_callback}, - {"log", lua::api::c2_log}, - {"later", lua::api::c2_later}, - {nullptr, nullptr}, - }; - lua_pushglobaltable(L); - auto gtable = lua_gettop(L); - - // count of elements in C2LIB + LogLevel + EventType - auto c2libIdx = lua::pushEmptyTable(L, 8); - - luaL_setfuncs(L, c2Lib, 0); - - lua::pushEnumTable(L); - lua_setfield(L, c2libIdx, "LogLevel"); - - lua::pushEnumTable(L); - lua_setfield(L, c2libIdx, "EventType"); - - lua::pushEnumTable(L); - lua_setfield(L, c2libIdx, "Platform"); - - lua::pushEnumTable(L); - lua_setfield(L, c2libIdx, "ChannelType"); - - lua::pushEnumTable(L); - lua_setfield(L, c2libIdx, "HTTPMethod"); - - // Initialize metatables for objects - lua::api::ChannelRef::createMetatable(L); - lua_setfield(L, c2libIdx, "Channel"); - - lua::api::HTTPRequest::createMetatable(L); - lua_setfield(L, c2libIdx, "HTTPRequest"); - - lua::api::HTTPResponse::createMetatable(L); - lua_setfield(L, c2libIdx, "HTTPResponse"); - - lua_setfield(L, gtable, "c2"); + auto r = lua.registry(); + auto g = lua.globals(); + auto c2 = lua.create_table(); + g["c2"] = c2; // ban functions // Note: this might not be fully secure? some kind of metatable fuckery might come up? - // possibly randomize this name at runtime to prevent some attacks? - # ifndef NDEBUG - lua_getfield(L, gtable, "load"); - lua_setfield(L, LUA_REGISTRYINDEX, "real_load"); + lua.registry()["real_load"] = lua.globals()["load"]; # endif + // See chatterino::lua::api::g_load implementation - // NOLINTNEXTLINE(*-avoid-c-arrays) - static const luaL_Reg replacementFuncs[] = { - {"load", lua::api::g_load}, - {"print", lua::api::g_print}, - {nullptr, nullptr}, - }; - luaL_setfuncs(L, replacementFuncs, 0); - - lua_pushnil(L); - lua_setfield(L, gtable, "loadfile"); - - lua_pushnil(L); - lua_setfield(L, gtable, "dofile"); + g["loadfile"] = sol::nil; + g["dofile"] = sol::nil; // set up package lib - lua_getfield(L, gtable, "package"); - - auto package = lua_gettop(L); - lua_pushstring(L, ""); - lua_setfield(L, package, "cpath"); - - // we don't use path - lua_pushstring(L, ""); - lua_setfield(L, package, "path"); - { - lua_getfield(L, gtable, "table"); - auto table = lua_gettop(L); - lua_getfield(L, -1, "remove"); - lua_remove(L, table); - } - auto remove = lua_gettop(L); + auto package = g["package"]; + package["cpath"] = ""; + package["path"] = ""; - // remove searcher_Croot, searcher_C and searcher_Lua leaving only searcher_preload - for (int i = 0; i < 3; i++) + sol::protected_function tbremove = g["table"]["remove"]; + + // remove searcher_Croot, searcher_C and searcher_Lua leaving only searcher_preload + sol::table searchers = package["searchers"]; + for (int i = 0; i < 3; i++) + { + tbremove(searchers); + } + searchers.add(&lua::api::searcherRelative); + searchers.add(&lua::api::searcherAbsolute); + } + // set up io lib { - lua_pushvalue(L, remove); - lua_getfield(L, package, "searchers"); - lua_pcall(L, 1, 0, 0); + auto c2io = lua.create_table(); + auto realio = r[lua::api::REG_REAL_IO_NAME]; + c2io["type"] = realio["type"]; + g["io"] = c2io; + // prevent plugins getting direct access to realio + r[LUA_LOADED_TABLE]["io"] = c2io; + + // Don't give plugins the option to shit into our stdio + r["_IO_input"] = sol::nil; + r["_IO_output"] = sol::nil; } - lua_pop(L, 1); // get rid of remove + PluginController::initSol(lua, plugin); +} - lua_getfield(L, package, "searchers"); - lua_pushcclosure(L, lua::api::searcherRelative, 0); - lua_seti(L, -2, 2); +// TODO: investigate if `plugin` can ever point to an invalid plugin, +// especially in cases when the plugin is errored. +void PluginController::initSol(sol::state_view &lua, Plugin *plugin) +{ + auto g = lua.globals(); + // Do not capture plugin->state_ in lambdas, this makes the functions unusable in callbacks + g.set_function("print", &lua::api::g_print); + g.set_function("load", &lua::api::g_load); - lua::push(L, QString(pluginDir.absolutePath())); - lua_pushcclosure(L, lua::api::searcherAbsolute, 1); - lua_seti(L, -2, 3); - lua_pop(L, 2); // remove package, package.searchers + sol::table c2 = g["c2"]; + c2.set_function("register_command", + [plugin](const QString &name, sol::protected_function cb) { + return plugin->registerCommand(name, std::move(cb)); + }); + c2.set_function("register_callback", &lua::api::c2_register_callback); + c2.set_function("log", &lua::api::c2_log); + c2.set_function("later", &lua::api::c2_later); - // NOLINTNEXTLINE(*-avoid-c-arrays) - static const luaL_Reg ioLib[] = { - {"close", lua::api::io_close}, - {"flush", lua::api::io_flush}, - {"input", lua::api::io_input}, - {"lines", lua::api::io_lines}, - {"open", lua::api::io_open}, - {"output", lua::api::io_output}, - {"popen", lua::api::io_popen}, // stub - {"read", lua::api::io_read}, - {"tmpfile", lua::api::io_tmpfile}, // stub - {"write", lua::api::io_write}, - // type = realio.type - {nullptr, nullptr}, - }; - // TODO: io.popen stub - auto iolibIdx = lua::pushEmptyTable(L, 1); - luaL_setfuncs(L, ioLib, 0); + lua::api::ChannelRef::createUserType(c2); + lua::api::HTTPResponse::createUserType(c2); + lua::api::HTTPRequest::createUserType(c2); + c2["ChannelType"] = lua::createEnumTable(lua); + c2["HTTPMethod"] = lua::createEnumTable(lua); + c2["EventType"] = lua::createEnumTable(lua); + c2["LogLevel"] = lua::createEnumTable(lua); - // set ourio.type = realio.type - lua_pushvalue(L, iolibIdx); - lua_getfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME); - lua_getfield(L, -1, "type"); - lua_remove(L, -2); // remove realio - lua_setfield(L, iolibIdx, "type"); - lua_pop(L, 1); // still have iolib on top of stack - - lua_pushvalue(L, iolibIdx); - lua_setfield(L, gtable, "io"); - - lua_pushvalue(L, iolibIdx); - lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_C2_IO_NAME); - - luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); - lua_pushvalue(L, iolibIdx); - lua_setfield(L, -2, "io"); - - lua_pop(L, 3); // remove gtable, iolib, LOADED - - // Don't give plugins the option to shit into our stdio - lua_pushnil(L); - lua_setfield(L, LUA_REGISTRYINDEX, "_IO_input"); - - lua_pushnil(L); - lua_setfield(L, LUA_REGISTRYINDEX, "_IO_output"); + sol::table io = g["io"]; + io.set_function( + "open", sol::overload(&lua::api::io_open, &lua::api::io_open_modeless)); + io.set_function("lines", sol::overload(&lua::api::io_lines, + &lua::api::io_lines_noargs)); + io.set_function("input", sol::overload(&lua::api::io_input_argless, + &lua::api::io_input_name, + &lua::api::io_input_file)); + io.set_function("output", sol::overload(&lua::api::io_output_argless, + &lua::api::io_output_name, + &lua::api::io_output_file)); + io.set_function("close", sol::overload(&lua::api::io_close_argless, + &lua::api::io_close_file)); + io.set_function("flush", sol::overload(&lua::api::io_flush_argless, + &lua::api::io_flush_file)); + io.set_function("read", &lua::api::io_read); + io.set_function("write", &lua::api::io_write); + io.set_function("popen", &lua::api::io_popen); + io.set_function("tmpfile", &lua::api::io_tmpfile); } void PluginController::load(const QFileInfo &index, const QDir &pluginDir, @@ -314,7 +262,7 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir, << " because safe mode is enabled."; return; } - PluginController::openLibrariesFor(l, meta, pluginDir); + PluginController::openLibrariesFor(temp); if (!PluginController::isPluginEnabled(pluginName) || !getSettings()->pluginsEnabled) @@ -345,17 +293,13 @@ bool PluginController::reload(const QString &id) { return false; } - if (it->second->state_ != nullptr) - { - lua_close(it->second->state_); - it->second->state_ = nullptr; - } + for (const auto &[cmd, _] : it->second->ownedCommands) { getApp()->getCommands()->unregisterPluginCommand(cmd); } - it->second->ownedCommands.clear(); QDir loadDir = it->second->loadDirectory_; + // Since Plugin owns the state, it will clean up everything related to it this->plugins_.erase(id); this->tryLoadFromDir(loadDir); return true; @@ -369,27 +313,36 @@ QString PluginController::tryExecPluginCommand(const QString &commandName, if (auto it = plugin->ownedCommands.find(commandName); it != plugin->ownedCommands.end()) { - const auto &funcName = it->second; + sol::state_view lua(plugin->state_); + sol::table args = lua.create_table_with( + "words", ctx.words, // + "channel", lua::api::ChannelRef(ctx.channel) // + ); - auto *L = plugin->state_; - lua_getfield(L, LUA_REGISTRYINDEX, funcName.toStdString().c_str()); - lua::push(L, ctx); - - auto res = lua_pcall(L, 1, 0, 0); - if (res != LUA_OK) + auto result = + lua::tryCall>(it->second, args); + if (!result) { - ctx.channel->addSystemMessage("Lua error: " + - lua::humanErrorText(L, res)); - return ""; + ctx.channel->addSystemMessage( + QStringView( + u"Failed to evaluate command from plugin %1: %2") + .arg(plugin->meta.name, result.error())); + return {}; } - return ""; + + auto opt = result.value(); + if (!opt) + { + return {}; + } + return *opt; } } qCCritical(chatterinoLua) << "Something's seriously up, no plugin owns command" << commandName << "yet a call to execute it came in"; assert(false && "missing plugin command owner"); - return ""; + return {}; } bool PluginController::isPluginEnabled(const QString &id) @@ -435,32 +388,31 @@ std::pair PluginController::updateCustomCompletions( continue; } - lua::StackGuard guard(pl->state_); - auto opt = pl->getCompletionCallback(); if (opt) { qCDebug(chatterinoLua) << "Processing custom completions from plugin" << name; auto &cb = *opt; - auto errOrList = cb(lua::api::CompletionEvent{ - .query = query, - .full_text_content = fullTextContent, - .cursor_position = cursorPosition, - .is_first_word = isFirstWord, - }); - if (std::holds_alternative(errOrList)) + sol::state_view view(pl->state_); + auto errOrList = lua::tryCall( + cb, + toTable(pl->state_, lua::api::CompletionEvent{ + .query = query, + .full_text_content = fullTextContent, + .cursor_position = cursorPosition, + .is_first_word = isFirstWord, + })); + if (!errOrList.has_value()) { - guard.handled(); - int err = std::get(errOrList); qCDebug(chatterinoLua) << "Got error from plugin " << pl->meta.name << " while refreshing tab completion: " - << lua::humanErrorText(pl->state_, err); + << errOrList.get_unexpected().error(); continue; } - auto list = std::get(errOrList); + auto list = lua::api::CompletionList(*errOrList); if (list.hideOthers) { results = QStringList(list.values.begin(), list.values.end()); diff --git a/src/controllers/plugins/PluginController.hpp b/src/controllers/plugins/PluginController.hpp index 50bc88c7e..ce4fe6170 100644 --- a/src/controllers/plugins/PluginController.hpp +++ b/src/controllers/plugins/PluginController.hpp @@ -10,6 +10,7 @@ # include # include # include +# include # include # include @@ -66,11 +67,16 @@ private: const PluginMeta &meta); // This function adds lua standard libraries into the state - static void openLibrariesFor(lua_State *L, const PluginMeta & /*meta*/, - const QDir &pluginDir); + static void openLibrariesFor(Plugin *plugin); + + static void initSol(sol::state_view &lua, Plugin *plugin); + static void loadChatterinoLib(lua_State *l); bool tryLoadFromDir(const QDir &pluginDir); std::map> plugins_; + + // This is for tests, pay no attention + friend class PluginControllerAccess; }; } // namespace chatterino diff --git a/src/controllers/plugins/PluginPermission.hpp b/src/controllers/plugins/PluginPermission.hpp index ffa728e7b..8beb918a1 100644 --- a/src/controllers/plugins/PluginPermission.hpp +++ b/src/controllers/plugins/PluginPermission.hpp @@ -10,6 +10,8 @@ namespace chatterino { struct PluginPermission { explicit PluginPermission(const QJsonObject &obj); + // This is for tests + PluginPermission() = default; enum class Type { FilesystemRead, diff --git a/src/controllers/plugins/SolTypes.cpp b/src/controllers/plugins/SolTypes.cpp new file mode 100644 index 000000000..43de08295 --- /dev/null +++ b/src/controllers/plugins/SolTypes.cpp @@ -0,0 +1,131 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/SolTypes.hpp" + +# include "controllers/plugins/PluginController.hpp" + +# include +# include +namespace chatterino::lua { + +Plugin *ThisPluginState::plugin() +{ + if (this->plugptr_ != nullptr) + { + return this->plugptr_; + } + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(this->state_); + if (pl == nullptr) + { + throw std::runtime_error("internal error: missing plugin"); + } + this->plugptr_ = pl; + return pl; +} + +} // namespace chatterino::lua + +// NOLINTBEGIN(readability-named-parameter) +// QString +bool sol_lua_check(sol::types, lua_State *L, int index, + std::function handler, + sol::stack::record &tracking) +{ + return sol::stack::check(L, index, handler, tracking); +} + +QString sol_lua_get(sol::types, lua_State *L, int index, + sol::stack::record &tracking) +{ + auto str = sol::stack::get(L, index, tracking); + return QString::fromUtf8(str.data(), static_cast(str.length())); +} + +int sol_lua_push(sol::types, lua_State *L, const QString &value) +{ + return sol::stack::push(L, value.toUtf8().data()); +} + +// QStringList +bool sol_lua_check(sol::types, lua_State *L, int index, + std::function handler, + sol::stack::record &tracking) +{ + return sol::stack::check(L, index, handler, tracking); +} + +QStringList sol_lua_get(sol::types, lua_State *L, int index, + sol::stack::record &tracking) +{ + sol::table table = sol::stack::get(L, index, tracking); + QStringList result; + result.reserve(static_cast(table.size())); + for (size_t i = 1; i < table.size() + 1; i++) + { + result.append(table.get(i)); + } + return result; +} + +int sol_lua_push(sol::types, lua_State *L, + const QStringList &value) +{ + sol::table table = sol::table::create(L, static_cast(value.size())); + for (const QString &str : value) + { + table.add(str); + } + return sol::stack::push(L, table); +} + +// QByteArray +bool sol_lua_check(sol::types, lua_State *L, int index, + std::function handler, + sol::stack::record &tracking) +{ + return sol::stack::check(L, index, handler, tracking); +} + +QByteArray sol_lua_get(sol::types, lua_State *L, int index, + sol::stack::record &tracking) +{ + auto str = sol::stack::get(L, index, tracking); + return QByteArray::fromRawData(str.data(), str.length()); +} + +int sol_lua_push(sol::types, lua_State *L, const QByteArray &value) +{ + return sol::stack::push(L, + std::string_view(value.constData(), value.size())); +} + +namespace chatterino::lua { + +// ThisPluginState + +bool sol_lua_check(sol::types, + lua_State * /*L*/, int /* index*/, + std::function /* handler*/, + sol::stack::record & /*tracking*/) +{ + return true; +} + +chatterino::lua::ThisPluginState sol_lua_get( + sol::types, lua_State *L, int /*index*/, + sol::stack::record &tracking) +{ + tracking.use(0); + return {L}; +} + +int sol_lua_push(sol::types, lua_State *L, + const chatterino::lua::ThisPluginState &value) +{ + return sol::stack::push(L, sol::thread(L, value)); +} + +} // namespace chatterino::lua + +// NOLINTEND(readability-named-parameter) + +#endif diff --git a/src/controllers/plugins/SolTypes.hpp b/src/controllers/plugins/SolTypes.hpp new file mode 100644 index 000000000..3ee41c3a7 --- /dev/null +++ b/src/controllers/plugins/SolTypes.hpp @@ -0,0 +1,170 @@ +#pragma once +#ifdef CHATTERINO_HAVE_PLUGINS +# include "util/QMagicEnum.hpp" +# include "util/TypeName.hpp" + +# include +# include +# include +# include +# include +# include + +namespace chatterino::detail { + +// NOLINTBEGIN(readability-identifier-naming) +template +constexpr bool IsOptional = false; +template +constexpr bool IsOptional> = true; +// NOLINTEND(readability-identifier-naming) + +} // namespace chatterino::detail + +namespace chatterino { + +class Plugin; + +} // namespace chatterino + +namespace chatterino::lua { + +class ThisPluginState +{ +public: + ThisPluginState(lua_State *Ls) + : plugptr_(nullptr) + , state_(Ls) + { + } + + operator lua_State *() const noexcept + { + return this->state_; + } + + lua_State *operator->() const noexcept + { + return this->state_; + } + lua_State *state() const noexcept + { + return this->state_; + } + + Plugin *plugin(); + +private: + Plugin *plugptr_; + lua_State *state_; +}; + +/// @brief Attempts to call @a function with @a args +/// +/// @a T is expected to be returned. +/// If `void` is specified, the returned values +/// are ignored. +/// `std::optional` means nil|LuaEquiv (or zero returns) +/// A return type that doesn't match returns an error +template +inline nonstd::expected_lite::expected tryCall( + const sol::protected_function &function, Args &&...args) +{ + sol::protected_function_result result = + function(std::forward(args)...); + if (!result.valid()) + { + sol::error err = result; + return nonstd::expected_lite::make_unexpected( + QString::fromUtf8(err.what())); + } + + if constexpr (std::is_same_v) + { + return {}; + } + else + { + if constexpr (detail::IsOptional) + { + if (result.return_count() == 0) + { + return {}; + } + } + if (result.return_count() > 1) + { + return nonstd::expected_lite::make_unexpected( + u"Expected one value to be returned but " % + QString::number(result.return_count()) % + u" values were returned"); + } + + try + { + if constexpr (detail::IsOptional) + { + // we want to error on anything that is not nil|T, + // std::optional in sol means "give me a T or if it does not match nullopt" + if (result.get_type() == sol::type::nil) + { + return {}; + } + auto ret = result.get(); + + if (!ret) + { + auto t = type_name(); + return nonstd::expected_lite::make_unexpected( + u"Expected " % QLatin1String(t.data(), t.size()) % + u" to be returned but " % + qmagicenum::enumName(result.get_type()) % + u" was returned"); + } + return *ret; + } + else + { + auto ret = result.get>(); + + if (!ret) + { + auto t = type_name(); + return nonstd::expected_lite::make_unexpected( + u"Expected " % QLatin1String(t.data(), t.size()) % + u" to be returned but " % + qmagicenum::enumName(result.get_type()) % + u" was returned"); + } + return *ret; + } + } + catch (std::runtime_error &e) + { + return nonstd::expected_lite::make_unexpected( + QString::fromUtf8(e.what())); + } + // non other exceptions we let it explode + } +} + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +# define SOL_STACK_FUNCTIONS(TYPE) \ + bool sol_lua_check(sol::types, lua_State *L, int index, \ + std::function handler, \ + sol::stack::record &tracking); \ + TYPE sol_lua_get(sol::types, lua_State *L, int index, \ + sol::stack::record &tracking); \ + int sol_lua_push(sol::types, lua_State *L, const TYPE &value); + +SOL_STACK_FUNCTIONS(chatterino::lua::ThisPluginState) + +} // namespace chatterino::lua + +SOL_STACK_FUNCTIONS(QString) +SOL_STACK_FUNCTIONS(QStringList) +SOL_STACK_FUNCTIONS(QByteArray) + +# undef SOL_STACK_FUNCTIONS + +#endif diff --git a/src/controllers/plugins/api/ChannelRef.cpp b/src/controllers/plugins/api/ChannelRef.cpp index a57e60119..b9bced3a0 100644 --- a/src/controllers/plugins/api/ChannelRef.cpp +++ b/src/controllers/plugins/api/ChannelRef.cpp @@ -1,397 +1,224 @@ #ifdef CHATTERINO_HAVE_PLUGINS # include "controllers/plugins/api/ChannelRef.hpp" +# include "Application.hpp" # include "common/Channel.hpp" # include "controllers/commands/CommandController.hpp" -# include "controllers/plugins/LuaAPI.hpp" -# include "controllers/plugins/LuaUtilities.hpp" -# include "messages/MessageBuilder.hpp" +# include "controllers/plugins/SolTypes.hpp" # include "providers/twitch/TwitchChannel.hpp" # include "providers/twitch/TwitchIrcServer.hpp" -extern "C" { -# include -# include -} +# include -# include # include # include namespace chatterino::lua::api { -// NOLINTBEGIN(*vararg) -// NOLINTNEXTLINE(*-avoid-c-arrays) -static const luaL_Reg CHANNEL_REF_METHODS[] = { - {"is_valid", &ChannelRef::is_valid}, - {"get_name", &ChannelRef::get_name}, - {"get_type", &ChannelRef::get_type}, - {"get_display_name", &ChannelRef::get_display_name}, - {"send_message", &ChannelRef::send_message}, - {"add_system_message", &ChannelRef::add_system_message}, - {"is_twitch_channel", &ChannelRef::is_twitch_channel}, - - // Twitch - {"get_room_modes", &ChannelRef::get_room_modes}, - {"get_stream_status", &ChannelRef::get_stream_status}, - {"get_twitch_id", &ChannelRef::get_twitch_id}, - {"is_broadcaster", &ChannelRef::is_broadcaster}, - {"is_mod", &ChannelRef::is_mod}, - {"is_vip", &ChannelRef::is_vip}, - - // misc - {"__tostring", &ChannelRef::to_string}, - - // static - {"by_name", &ChannelRef::get_by_name}, - {"by_twitch_id", &ChannelRef::get_by_twitch_id}, - {nullptr, nullptr}, -}; - -void ChannelRef::createMetatable(lua_State *L) +ChannelRef::ChannelRef(const std::shared_ptr &chan) + : weak(chan) { - lua::StackGuard guard(L, 1); - - luaL_newmetatable(L, "c2.Channel"); - lua_pushstring(L, "__index"); - lua_pushvalue(L, -2); // clone metatable - lua_settable(L, -3); // metatable.__index = metatable - - // Generic IWeakResource stuff - lua_pushstring(L, "__gc"); - lua_pushcfunction( - L, (&WeakPtrUserData::destroy)); - lua_settable(L, -3); // metatable.__gc = WeakPtrUserData<...>::destroy - - luaL_setfuncs(L, CHANNEL_REF_METHODS, 0); } -ChannelPtr ChannelRef::getOrError(lua_State *L, bool expiredOk) +std::shared_ptr ChannelRef::strong() { - if (lua_gettop(L) < 1) + auto c = this->weak.lock(); + if (!c) { - luaL_error(L, "Called c2.Channel method without a channel object"); - return nullptr; + throw std::runtime_error( + "Expired c2.Channel used - use c2.Channel:is_valid() to " + "check validity"); } - if (lua_isuserdata(L, lua_gettop(L)) == 0) + return c; +} + +std::shared_ptr ChannelRef::twitch() +{ + auto c = std::dynamic_pointer_cast(this->weak.lock()); + if (!c) { - luaL_error( - L, "Called c2.Channel method with a non-userdata 'self' argument"); - return nullptr; + throw std::runtime_error( + "Expired or non-twitch c2.Channel used - use " + "c2.Channel:is_valid() and c2.Channe:is_twitch_channel()"); } - // luaL_checkudata is no-return if check fails - auto *checked = luaL_checkudata(L, lua_gettop(L), "c2.Channel"); - auto *data = - WeakPtrUserData::from(checked); - if (data == nullptr) - { - luaL_error(L, - "Called c2.Channel method with an invalid channel pointer"); - return nullptr; - } - lua_pop(L, 1); - if (data->target.expired()) - { - if (!expiredOk) + return c; +} + +bool ChannelRef::is_valid() +{ + return !this->weak.expired(); +} + +QString ChannelRef::get_name() +{ + return this->strong()->getName(); +} + +Channel::Type ChannelRef::get_type() +{ + return this->strong()->getType(); +} + +QString ChannelRef::get_display_name() +{ + return this->strong()->getDisplayName(); +} + +void ChannelRef::send_message(QString text, sol::variadic_args va) +{ + bool execCommands = [&] { + if (va.size() >= 1) { - luaL_error(L, - "Usage of expired c2.Channel object. Underlying " - "resource was freed. Use Channel:is_valid() to check"); + return va.get(); } - return nullptr; - } - return data->target.lock(); -} - -std::shared_ptr ChannelRef::getTwitchOrError(lua_State *L) -{ - auto ref = ChannelRef::getOrError(L); - auto ptr = dynamic_pointer_cast(ref); - if (ptr == nullptr) + return false; + }(); + text = text.replace('\n', ' '); + auto chan = this->strong(); + if (execCommands) { - luaL_error(L, - "c2.Channel Twitch-only operation on non-Twitch channel."); + text = getApp()->getCommands()->execCommand(text, chan, false); } - return ptr; + chan->sendMessage(text); } -int ChannelRef::is_valid(lua_State *L) +void ChannelRef::add_system_message(QString text) { - ChannelPtr that = ChannelRef::getOrError(L, true); - lua::push(L, that != nullptr); - return 1; + text = text.replace('\n', ' '); + this->strong()->addSystemMessage(text); } -int ChannelRef::get_name(lua_State *L) +bool ChannelRef::is_twitch_channel() { - ChannelPtr that = ChannelRef::getOrError(L); - lua::push(L, that->getName()); - return 1; + return this->strong()->isTwitchChannel(); } -int ChannelRef::get_type(lua_State *L) +sol::table ChannelRef::get_room_modes(sol::this_state state) { - ChannelPtr that = ChannelRef::getOrError(L); - lua::push(L, that->getType()); - return 1; + return toTable(state.L, *this->twitch()->accessRoomModes()); } -int ChannelRef::get_display_name(lua_State *L) +sol::table ChannelRef::get_stream_status(sol::this_state state) { - ChannelPtr that = ChannelRef::getOrError(L); - lua::push(L, that->getDisplayName()); - return 1; + return toTable(state.L, *this->twitch()->accessStreamStatus()); } -int ChannelRef::send_message(lua_State *L) +QString ChannelRef::get_twitch_id() { - if (lua_gettop(L) != 2 && lua_gettop(L) != 3) + return this->twitch()->roomId(); +} + +bool ChannelRef::is_broadcaster() +{ + return this->twitch()->isBroadcaster(); +} + +bool ChannelRef::is_mod() +{ + return this->twitch()->isMod(); +} + +bool ChannelRef::is_vip() +{ + return this->twitch()->isVip(); +} + +QString ChannelRef::to_string() +{ + auto chan = this->weak.lock(); + if (!chan) { - luaL_error(L, "Channel:send_message needs 1 or 2 arguments (message " - "text and optionally execute_commands flag)"); - return 0; + return ""; } - bool execcmds = false; - if (lua_gettop(L) == 3) + return QStringView(u"").arg(chan->getName()); +} + +std::optional ChannelRef::get_by_name(const QString &name) +{ + auto chan = getApp()->getTwitch()->getChannelOrEmpty(name); + if (chan->isEmpty()) { - if (!lua::pop(L, &execcmds)) + return std::nullopt; + } + return chan; +} + +std::optional ChannelRef::get_by_twitch_id(const QString &id) +{ + auto chan = getApp()->getTwitch()->getChannelOrEmptyByID(id); + if (chan->isEmpty()) + { + return std::nullopt; + } + return chan; +} + +void ChannelRef::createUserType(sol::table &c2) +{ + // clang-format off + c2.new_usertype( + "Channel", sol::no_constructor, + // meta methods + sol::meta_method::to_string, &ChannelRef::to_string, + + // Channel + "is_valid", &ChannelRef::is_valid, + "get_name",&ChannelRef::get_name, + "get_type", &ChannelRef::get_type, + "get_display_name", &ChannelRef::get_display_name, + "send_message", &ChannelRef::send_message, + "add_system_message", &ChannelRef::add_system_message, + "is_twitch_channel", &ChannelRef::is_twitch_channel, + + // TwitchChannel + "get_room_modes", &ChannelRef::get_room_modes, + "get_stream_status", &ChannelRef::get_stream_status, + "get_twitch_id", &ChannelRef::get_twitch_id, + "is_broadcaster", &ChannelRef::is_broadcaster, + "is_mod", &ChannelRef::is_mod, + "is_vip", &ChannelRef::is_vip, + + // static + "by_name", &ChannelRef::get_by_name, + "by_twitch_id", &ChannelRef::get_by_twitch_id + ); + // clang-format on +} + +sol::table toTable(lua_State *L, const TwitchChannel::RoomModes &modes) +{ + auto maybe = [](int value) { + if (value >= 0) { - luaL_error(L, "cannot get execute_commands (2nd argument of " - "Channel:send_message)"); - return 0; + return std::optional{value}; } - } - - QString text; - if (!lua::pop(L, &text)) - { - luaL_error(L, "cannot get text (1st argument of Channel:send_message)"); - return 0; - } - - ChannelPtr that = ChannelRef::getOrError(L); - - text = text.replace('\n', ' '); - if (execcmds) - { - text = getApp()->getCommands()->execCommand(text, that, false); - } - that->sendMessage(text); - return 0; -} - -int ChannelRef::add_system_message(lua_State *L) -{ - // needs to account for the hidden self argument - if (lua_gettop(L) != 2) - { - luaL_error( - L, "Channel:add_system_message needs exactly 1 argument (message " - "text)"); - return 0; - } - - QString text; - if (!lua::pop(L, &text)) - { - luaL_error( - L, "cannot get text (1st argument of Channel:add_system_message)"); - return 0; - } - ChannelPtr that = ChannelRef::getOrError(L); - text = text.replace('\n', ' '); - that->addSystemMessage(text); - return 0; -} - -int ChannelRef::is_twitch_channel(lua_State *L) -{ - ChannelPtr that = ChannelRef::getOrError(L); - lua::push(L, that->isTwitchChannel()); - return 1; -} - -int ChannelRef::get_room_modes(lua_State *L) -{ - auto tc = ChannelRef::getTwitchOrError(L); - const auto m = tc->accessRoomModes(); - const auto modes = LuaRoomModes{ - .unique_chat = m->r9k, - .subscriber_only = m->submode, - .emotes_only = m->emoteOnly, - .follower_only = (m->followerOnly == -1) - ? std::nullopt - : std::optional(m->followerOnly), - .slow_mode = - (m->slowMode == 0) ? std::nullopt : std::optional(m->slowMode), - + return std::optional{}; }; - lua::push(L, modes); - return 1; + // clang-format off + return sol::table::create_with(L, + "subscriber_only", modes.submode, + "unique_chat", modes.r9k, + "emotes_only", modes.emoteOnly, + "follower_only", maybe(modes.followerOnly), + "slow_mode", maybe(modes.slowMode) + ); + // clang-format on } -int ChannelRef::get_stream_status(lua_State *L) +sol::table toTable(lua_State *L, const TwitchChannel::StreamStatus &status) { - auto tc = ChannelRef::getTwitchOrError(L); - const auto s = tc->accessStreamStatus(); - const auto status = LuaStreamStatus{ - .live = s->live, - .viewer_count = static_cast(s->viewerCount), - .uptime = s->uptimeSeconds, - .title = s->title, - .game_name = s->game, - .game_id = s->gameId, - }; - lua::push(L, status); - return 1; + // clang-format off + return sol::table::create_with(L, + "live", status.live, + "viewer_count", status.viewerCount, + "title", status.title, + "game_name", status.game, + "game_id", status.gameId, + "uptime", status.uptimeSeconds + ); + // clang-format on } -int ChannelRef::get_twitch_id(lua_State *L) -{ - auto tc = ChannelRef::getTwitchOrError(L); - lua::push(L, tc->roomId()); - return 1; -} - -int ChannelRef::is_broadcaster(lua_State *L) -{ - auto tc = ChannelRef::getTwitchOrError(L); - lua::push(L, tc->isBroadcaster()); - return 1; -} - -int ChannelRef::is_mod(lua_State *L) -{ - auto tc = ChannelRef::getTwitchOrError(L); - lua::push(L, tc->isMod()); - return 1; -} - -int ChannelRef::is_vip(lua_State *L) -{ - auto tc = ChannelRef::getTwitchOrError(L); - lua::push(L, tc->isVip()); - return 1; -} - -int ChannelRef::get_by_name(lua_State *L) -{ - if (lua_gettop(L) != 2) - { - luaL_error(L, "Channel.by_name needs exactly 2 arguments (channel " - "name and platform)"); - lua_pushnil(L); - return 1; - } - LPlatform platform{}; - if (!lua::pop(L, &platform)) - { - luaL_error(L, "cannot get platform (2nd argument of Channel.by_name, " - "expected a string)"); - lua_pushnil(L); - return 1; - } - QString name; - if (!lua::pop(L, &name)) - { - luaL_error(L, - "cannot get channel name (1st argument of Channel.by_name, " - "expected a string)"); - lua_pushnil(L); - return 1; - } - auto chn = getApp()->getTwitch()->getChannelOrEmpty(name); - lua::push(L, chn); - return 1; -} - -int ChannelRef::get_by_twitch_id(lua_State *L) -{ - if (lua_gettop(L) != 1) - { - luaL_error( - L, "Channel.by_twitch_id needs exactly 1 arguments (channel owner " - "id)"); - lua_pushnil(L); - return 1; - } - QString id; - if (!lua::pop(L, &id)) - { - luaL_error(L, - "cannot get channel name (1st argument of Channel.by_name, " - "expected a string)"); - lua_pushnil(L); - return 1; - } - auto chn = getApp()->getTwitch()->getChannelOrEmptyByID(id); - - lua::push(L, chn); - return 1; -} - -int ChannelRef::to_string(lua_State *L) -{ - ChannelPtr that = ChannelRef::getOrError(L, true); - if (that == nullptr) - { - lua_pushstring(L, ""); - return 1; - } - QString formated = QString("").arg(that->getName()); - lua::push(L, formated); - return 1; -} } // namespace chatterino::lua::api -// NOLINTEND(*vararg) -// -namespace chatterino::lua { -StackIdx push(lua_State *L, const api::LuaRoomModes &modes) -{ - auto out = lua::pushEmptyTable(L, 6); -# define PUSH(field) \ - lua::push(L, modes.field); \ - lua_setfield(L, out, #field) - PUSH(unique_chat); - PUSH(subscriber_only); - PUSH(emotes_only); - PUSH(follower_only); - PUSH(slow_mode); -# undef PUSH - return out; -} -StackIdx push(lua_State *L, const api::LuaStreamStatus &status) -{ - auto out = lua::pushEmptyTable(L, 6); -# define PUSH(field) \ - lua::push(L, status.field); \ - lua_setfield(L, out, #field) - PUSH(live); - PUSH(viewer_count); - PUSH(uptime); - PUSH(title); - PUSH(game_name); - PUSH(game_id); -# undef PUSH - return out; -} - -StackIdx push(lua_State *L, ChannelPtr chn) -{ - using namespace chatterino::lua::api; - - if (chn->isEmpty()) - { - lua_pushnil(L); - return lua_gettop(L); - } - WeakPtrUserData::create( - L, chn->weak_from_this()); - luaL_getmetatable(L, "c2.Channel"); - lua_setmetatable(L, -2); - return lua_gettop(L); -} - -} // namespace chatterino::lua #endif diff --git a/src/controllers/plugins/api/ChannelRef.hpp b/src/controllers/plugins/api/ChannelRef.hpp index 32e1946ab..9d4455739 100644 --- a/src/controllers/plugins/api/ChannelRef.hpp +++ b/src/controllers/plugins/api/ChannelRef.hpp @@ -1,48 +1,24 @@ #pragma once #ifdef CHATTERINO_HAVE_PLUGINS # include "common/Channel.hpp" -# include "controllers/plugins/LuaUtilities.hpp" -# include "controllers/plugins/PluginController.hpp" # include "providers/twitch/TwitchChannel.hpp" -# include +# include namespace chatterino::lua::api { // NOLINTBEGIN(readability-identifier-naming) /** - * This enum describes a platform for the purpose of searching for a channel. - * Currently only Twitch is supported because identifying IRC channels is tricky. - * @exposeenum c2.Platform + * @includefile providers/twitch/TwitchChannel.hpp */ -enum class LPlatform { - Twitch, - //IRC, -}; /** * @lua@class c2.Channel */ struct ChannelRef { - static void createMetatable(lua_State *L); - friend class chatterino::PluginController; - - /** - * @brief Get the content of the top object on Lua stack, usually first argument to function as a ChannelPtr. - * If the object given is not a userdatum or the pointer inside that - * userdatum doesn't point to a Channel, a lua error is thrown. - * - * @param expiredOk Should an expired return nullptr instead of erroring - */ - static ChannelPtr getOrError(lua_State *L, bool expiredOk = false); - - /** - * @brief Casts the result of getOrError to std::shared_ptr - * if that fails thows a lua error. - */ - static std::shared_ptr getTwitchOrError(lua_State *L); - public: + ChannelRef(const std::shared_ptr &chan); + /** * Returns true if the channel this object points to is valid. * If the object expired, returns false @@ -51,7 +27,7 @@ public: * @lua@return boolean success * @exposed c2.Channel:is_valid */ - static int is_valid(lua_State *L); + bool is_valid(); /** * Gets the channel's name. This is the lowercase login name. @@ -59,7 +35,7 @@ public: * @lua@return string name * @exposed c2.Channel:get_name */ - static int get_name(lua_State *L); + QString get_name(); /** * Gets the channel's type @@ -67,7 +43,7 @@ public: * @lua@return c2.ChannelType * @exposed c2.Channel:get_type */ - static int get_type(lua_State *L); + Channel::Type get_type(); /** * Get the channel owner's display name. This may contain non-lowercase ascii characters. @@ -75,17 +51,17 @@ public: * @lua@return string name * @exposed c2.Channel:get_display_name */ - static int get_display_name(lua_State *L); + QString get_display_name(); /** * Sends a message to the target channel. * Note that this does not execute client-commands. * * @lua@param message string - * @lua@param execute_commands boolean Should commands be run on the text? + * @lua@param execute_commands? boolean Should commands be run on the text? * @exposed c2.Channel:send_message */ - static int send_message(lua_State *L); + void send_message(QString text, sol::variadic_args va); /** * Adds a system message client-side @@ -93,7 +69,7 @@ public: * @lua@param message string * @exposed c2.Channel:add_system_message */ - static int add_system_message(lua_State *L); + void add_system_message(QString text); /** * Returns true for twitch channels. @@ -103,7 +79,7 @@ public: * @lua@return boolean * @exposed c2.Channel:is_twitch_channel */ - static int is_twitch_channel(lua_State *L); + bool is_twitch_channel(); /** * Twitch Channel specific functions @@ -115,7 +91,7 @@ public: * @lua@return RoomModes * @exposed c2.Channel:get_room_modes */ - static int get_room_modes(lua_State *L); + sol::table get_room_modes(sol::this_state state); /** * Returns a copy of the stream status. @@ -123,7 +99,7 @@ public: * @lua@return StreamStatus * @exposed c2.Channel:get_stream_status */ - static int get_stream_status(lua_State *L); + sol::table get_stream_status(sol::this_state state); /** * Returns the Twitch user ID of the owner of the channel. @@ -131,7 +107,7 @@ public: * @lua@return string * @exposed c2.Channel:get_twitch_id */ - static int get_twitch_id(lua_State *L); + QString get_twitch_id(); /** * Returns true if the channel is a Twitch channel and the user owns it @@ -139,7 +115,7 @@ public: * @lua@return boolean * @exposed c2.Channel:is_broadcaster */ - static int is_broadcaster(lua_State *L); + bool is_broadcaster(); /** * Returns true if the channel is a Twitch channel and the user is a moderator in the channel @@ -148,7 +124,7 @@ public: * @lua@return boolean * @exposed c2.Channel:is_mod */ - static int is_mod(lua_State *L); + bool is_mod(); /** * Returns true if the channel is a Twitch channel and the user is a VIP in the channel @@ -157,7 +133,7 @@ public: * @lua@return boolean * @exposed c2.Channel:is_vip */ - static int is_vip(lua_State *L); + bool is_vip(); /** * Misc @@ -167,7 +143,7 @@ public: * @lua@return string * @exposed c2.Channel:__tostring */ - static int to_string(lua_State *L); + QString to_string(); /** * Static functions @@ -184,11 +160,10 @@ public: * - /automod * * @lua@param name string Which channel are you looking for? - * @lua@param platform c2.Platform Where to search for the channel? * @lua@return c2.Channel? * @exposed c2.Channel.by_name */ - static int get_by_name(lua_State *L); + static std::optional get_by_name(const QString &name); /** * Finds a channel by the Twitch user ID of its owner. @@ -197,79 +172,24 @@ public: * @lua@return c2.Channel? * @exposed c2.Channel.by_twitch_id */ - static int get_by_twitch_id(lua_State *L); -}; + static std::optional get_by_twitch_id(const QString &id); -// This is a copy of the TwitchChannel::RoomModes structure, except it uses nicer optionals -/** - * @lua@class RoomModes - */ -struct LuaRoomModes { - /** - * @lua@field unique_chat boolean You might know this as r9kbeta or robot9000. - */ - bool unique_chat = false; + static void createUserType(sol::table &c2); - /** - * @lua@field subscriber_only boolean - */ - bool subscriber_only = false; +private: + std::weak_ptr weak; - /** - * @lua@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes - */ - bool emotes_only = false; + /// Locks the weak pointer and throws if the pointer expired + std::shared_ptr strong(); - /** - * @lua@field follower_only number? Time in minutes you need to follow to chat or nil. - */ - std::optional follower_only; - /** - * @lua@field slow_mode number? Time in seconds you need to wait before sending messages or nil. - */ - std::optional slow_mode; -}; - -/** - * @lua@class StreamStatus - */ -struct LuaStreamStatus { - /** - * @lua@field live boolean - */ - bool live = false; - - /** - * @lua@field viewer_count number - */ - int viewer_count = 0; - - /** - * @lua@field uptime number Seconds since the stream started. - */ - int uptime = 0; - - /** - * @lua@field title string Stream title or last stream title - */ - QString title; - - /** - * @lua@field game_name string - */ - QString game_name; - - /** - * @lua@field game_id string - */ - QString game_id; + /// Locks the weak pointer and throws if the pointer is invalid + std::shared_ptr twitch(); }; // NOLINTEND(readability-identifier-naming) + +sol::table toTable(lua_State *L, const TwitchChannel::RoomModes &modes); +sol::table toTable(lua_State *L, const TwitchChannel::StreamStatus &status); + } // namespace chatterino::lua::api -namespace chatterino::lua { -StackIdx push(lua_State *L, const api::LuaRoomModes &modes); -StackIdx push(lua_State *L, const api::LuaStreamStatus &status); -StackIdx push(lua_State *L, ChannelPtr chn); -} // namespace chatterino::lua #endif diff --git a/src/controllers/plugins/api/EventType.hpp b/src/controllers/plugins/api/EventType.hpp new file mode 100644 index 000000000..73b5df135 --- /dev/null +++ b/src/controllers/plugins/api/EventType.hpp @@ -0,0 +1,14 @@ +#pragma once +#ifdef CHATTERINO_HAVE_PLUGINS + +namespace chatterino::lua::api { + +/** + * @exposeenum c2.EventType + */ +enum class EventType { + CompletionRequested, +}; + +} // namespace chatterino::lua::api +#endif diff --git a/src/controllers/plugins/api/HTTPRequest.cpp b/src/controllers/plugins/api/HTTPRequest.cpp index eba2773ad..a1a97f616 100644 --- a/src/controllers/plugins/api/HTTPRequest.cpp +++ b/src/controllers/plugins/api/HTTPRequest.cpp @@ -6,402 +6,173 @@ # include "common/network/NetworkRequest.hpp" # include "common/network/NetworkResult.hpp" # include "controllers/plugins/api/HTTPResponse.hpp" -# include "controllers/plugins/LuaAPI.hpp" # include "controllers/plugins/LuaUtilities.hpp" +# include "controllers/plugins/PluginController.hpp" +# include "controllers/plugins/SolTypes.hpp" # include "util/DebugCount.hpp" -extern "C" { # include # include -} +# include +# include # include # include +# include +# include +# include +# include +# include -# include +# include +# include # include +# include namespace chatterino::lua::api { -// NOLINTBEGIN(*vararg) -// NOLINTNEXTLINE(*-avoid-c-arrays) -static const luaL_Reg HTTP_REQUEST_METHODS[] = { - {"on_success", &HTTPRequest::on_success_wrap}, - {"on_error", &HTTPRequest::on_error_wrap}, - {"finally", &HTTPRequest::finally_wrap}, - {"execute", &HTTPRequest::execute_wrap}, - {"set_timeout", &HTTPRequest::set_timeout_wrap}, - {"set_payload", &HTTPRequest::set_payload_wrap}, - {"set_header", &HTTPRequest::set_header_wrap}, - // static - {"create", &HTTPRequest::create}, - {nullptr, nullptr}, -}; - -std::shared_ptr HTTPRequest::getOrError(lua_State *L, - StackIdx where) +void HTTPRequest::createUserType(sol::table &c2) { - if (lua_gettop(L) < 1) - { - // The nullptr is there just to appease the compiler, luaL_error is no return - luaL_error(L, "Called c2.HTTPRequest method without a request object"); - return nullptr; - } - if (lua_isuserdata(L, where) == 0) - { - luaL_error( - L, - "Called c2.HTTPRequest method with a non-userdata 'self' argument"); - return nullptr; - } - // luaL_checkudata is no-return if check fails - auto *checked = luaL_checkudata(L, where, "c2.HTTPRequest"); - auto *data = - SharedPtrUserData::from( - checked); - if (data == nullptr) - { - luaL_error(L, "Called c2.HTTPRequest method with an invalid pointer"); - return nullptr; - } - lua_remove(L, where); - if (data->target == nullptr) - { - luaL_error( - L, "Internal error: SharedPtrUserData::target was null. This is a Chatterino bug!"); - return nullptr; - } - if (data->target->done) - { - luaL_error(L, "This c2.HTTPRequest has already been executed!"); - return nullptr; - } - return data->target; + c2.new_usertype( // + "HTTPRequest", sol::no_constructor, // + sol::meta_method::to_string, &HTTPRequest::to_string, // + + "on_success", &HTTPRequest::on_success, // + "on_error", &HTTPRequest::on_error, // + "finally", &HTTPRequest::finally, // + + "set_timeout", &HTTPRequest::set_timeout, // + "set_payload", &HTTPRequest::set_payload, // + "set_header", &HTTPRequest::set_header, // + "execute", &HTTPRequest::execute, // + + "create", &HTTPRequest::create // + ); } -void HTTPRequest::createMetatable(lua_State *L) +void HTTPRequest::on_success(sol::protected_function func) { - lua::StackGuard guard(L, 1); - - luaL_newmetatable(L, "c2.HTTPRequest"); - lua_pushstring(L, "__index"); - lua_pushvalue(L, -2); // clone metatable - lua_settable(L, -3); // metatable.__index = metatable - - // Generic ISharedResource stuff - lua_pushstring(L, "__gc"); - lua_pushcfunction(L, (&SharedPtrUserData::destroy)); - lua_settable(L, -3); // metatable.__gc = SharedPtrUserData<...>::destroy - - luaL_setfuncs(L, HTTP_REQUEST_METHODS, 0); + this->cbSuccess = std::make_optional(func); } -int HTTPRequest::on_success_wrap(lua_State *L) +void HTTPRequest::on_error(sol::protected_function func) { - lua::StackGuard guard(L, -2); - auto ptr = HTTPRequest::getOrError(L, 1); - return ptr->on_success(L); + this->cbError = std::make_optional(func); } -int HTTPRequest::on_success(lua_State *L) +void HTTPRequest::set_timeout(int timeout) { - auto top = lua_gettop(L); - if (top != 1) - { - return luaL_error( - L, "HTTPRequest:on_success needs 1 argument (a callback " - "that takes an HTTPResult and doesn't return anything)"); - } - if (!lua_isfunction(L, top)) - { - return luaL_error( - L, "HTTPRequest:on_success needs 1 argument (a callback " - "that takes an HTTPResult and doesn't return anything)"); - } - auto shared = this->pushPrivate(L); - lua_pushvalue(L, -2); - lua_setfield(L, shared, "success"); // this deletes the function copy - lua_pop(L, 2); // delete the table and function original - return 0; + this->timeout_ = timeout; } -int HTTPRequest::on_error_wrap(lua_State *L) +void HTTPRequest::finally(sol::protected_function func) { - lua::StackGuard guard(L, -2); - auto ptr = HTTPRequest::getOrError(L, 1); - return ptr->on_error(L); + this->cbFinally = std::make_optional(func); } -int HTTPRequest::on_error(lua_State *L) +void HTTPRequest::set_payload(QByteArray payload) { - auto top = lua_gettop(L); - if (top != 1) - { - return luaL_error( - L, "HTTPRequest:on_error needs 1 argument (a callback " - "that takes an HTTPResult and doesn't return anything)"); - } - if (!lua_isfunction(L, top)) - { - return luaL_error( - L, "HTTPRequest:on_error needs 1 argument (a callback " - "that takes an HTTPResult and doesn't return anything)"); - } - auto shared = this->pushPrivate(L); - lua_pushvalue(L, -2); - lua_setfield(L, shared, "error"); // this deletes the function copy - lua_pop(L, 2); // delete the table and function original - return 0; + this->req_ = std::move(this->req_).payload(payload); } -int HTTPRequest::set_timeout_wrap(lua_State *L) +// name and value may be random bytes +void HTTPRequest::set_header(QByteArray name, QByteArray value) { - lua::StackGuard guard(L, -2); - auto ptr = HTTPRequest::getOrError(L, 1); - return ptr->set_timeout(L); + this->req_ = std::move(this->req_).header(name, value); } -int HTTPRequest::set_timeout(lua_State *L) +std::shared_ptr HTTPRequest::create(sol::this_state L, + NetworkRequestType method, + QString url) { - auto top = lua_gettop(L); - if (top != 1) - { - return luaL_error( - L, "HTTPRequest:set_timeout needs 1 argument (a number of " - "milliseconds after which the request will time out)"); - } - - int temporary = -1; - if (!lua::pop(L, &temporary)) - { - return luaL_error( - L, "HTTPRequest:set_timeout failed to get timeout, expected a " - "positive integer"); - } - if (temporary <= 0) - { - return luaL_error( - L, "HTTPRequest:set_timeout failed to get timeout, expected a " - "positive integer"); - } - this->timeout_ = temporary; - return 0; -} - -int HTTPRequest::finally_wrap(lua_State *L) -{ - lua::StackGuard guard(L, -2); - auto ptr = HTTPRequest::getOrError(L, 1); - return ptr->finally(L); -} - -int HTTPRequest::finally(lua_State *L) -{ - auto top = lua_gettop(L); - if (top != 1) - { - return luaL_error(L, "HTTPRequest:finally needs 1 argument (a callback " - "that takes nothing and doesn't return anything)"); - } - if (!lua_isfunction(L, top)) - { - return luaL_error(L, "HTTPRequest:finally needs 1 argument (a callback " - "that takes nothing and doesn't return anything)"); - } - auto shared = this->pushPrivate(L); - lua_pushvalue(L, -2); - lua_setfield(L, shared, "finally"); // this deletes the function copy - lua_pop(L, 2); // delete the table and function original - return 0; -} - -int HTTPRequest::set_payload_wrap(lua_State *L) -{ - lua::StackGuard guard(L, -2); - auto ptr = HTTPRequest::getOrError(L, 1); - return ptr->set_payload(L); -} - -int HTTPRequest::set_payload(lua_State *L) -{ - auto top = lua_gettop(L); - if (top != 1) - { - return luaL_error( - L, "HTTPRequest:set_payload needs 1 argument (a string payload)"); - } - - std::string temporary; - if (!lua::pop(L, &temporary)) - { - return luaL_error( - L, "HTTPRequest:set_payload failed to get payload, expected a " - "string"); - } - this->req_ = - std::move(this->req_).payload(QByteArray::fromStdString(temporary)); - return 0; -} - -int HTTPRequest::set_header_wrap(lua_State *L) -{ - lua::StackGuard guard(L, -3); - auto ptr = HTTPRequest::getOrError(L, 1); - return ptr->set_header(L); -} - -int HTTPRequest::set_header(lua_State *L) -{ - auto top = lua_gettop(L); - if (top != 2) - { - return luaL_error( - L, "HTTPRequest:set_header needs 2 arguments (a header name " - "and a value)"); - } - - std::string value; - if (!lua::pop(L, &value)) - { - return luaL_error( - L, "cannot get value (2nd argument of HTTPRequest:set_header)"); - } - std::string name; - if (!lua::pop(L, &name)) - { - return luaL_error( - L, "cannot get name (1st argument of HTTPRequest:set_header)"); - } - this->req_ = std::move(this->req_) - .header(QByteArray::fromStdString(name), - QByteArray::fromStdString(value)); - return 0; -} - -int HTTPRequest::create(lua_State *L) -{ - lua::StackGuard guard(L, -1); - if (lua_gettop(L) != 2) - { - return luaL_error( - L, "HTTPRequest.create needs exactly 2 arguments (method " - "and url)"); - } - QString url; - if (!lua::pop(L, &url)) - { - return luaL_error(L, - "cannot get url (2nd argument of HTTPRequest.create, " - "expected a string)"); - } auto parsedurl = QUrl(url); if (!parsedurl.isValid()) { - return luaL_error( - L, "cannot parse url (2nd argument of HTTPRequest.create, " - "got invalid url in argument)"); - } - NetworkRequestType method{}; - if (!lua::pop(L, &method)) - { - return luaL_error( - L, "cannot get method (1st argument of HTTPRequest.create, " - "expected a string)"); + throw std::runtime_error( + "cannot parse url (2nd argument of HTTPRequest.create, " + "got invalid url in argument)"); } auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); if (!pl->hasHTTPPermissionFor(parsedurl)) { - return luaL_error( - L, "Plugin does not have permission to send HTTP requests " - "to this URL"); + throw std::runtime_error( + "Plugin does not have permission to send HTTP requests " + "to this URL"); } NetworkRequest r(parsedurl, method); - lua::push( - L, std::make_shared(ConstructorAccessTag{}, std::move(r))); - return 1; + return std::make_shared(ConstructorAccessTag{}, std::move(r)); } -int HTTPRequest::execute_wrap(lua_State *L) +void HTTPRequest::execute(sol::this_state L) { - auto ptr = HTTPRequest::getOrError(L, 1); - return ptr->execute(L); -} - -int HTTPRequest::execute(lua_State *L) -{ - auto shared = this->shared_from_this(); + if (this->done) + { + throw std::runtime_error( + "Cannot execute this c2.HTTPRequest, it was executed already!"); + } this->done = true; + + // this keeps the object alive even if Lua were to forget about it, + auto hack = this->weak_from_this(); + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + pl->httpRequests.push_back(this->shared_from_this()); + std::move(this->req_) - .onSuccess([shared, L](const NetworkResult &res) { + .onSuccess([L, hack](const NetworkResult &res) { + auto self = hack.lock(); + if (!self) + { + return; + } + if (!self->cbSuccess.has_value()) + { + return; + } lua::StackGuard guard(L); - auto *thread = lua_newthread(L); - - auto priv = shared->pushPrivate(thread); - lua_getfield(thread, priv, "success"); - auto cb = lua_gettop(thread); - if (lua_isfunction(thread, cb)) - { - lua::push(thread, std::make_shared(res)); - // one arg, no return, no msgh - lua_pcall(thread, 1, 0, 0); - } - else - { - lua_pop(thread, 1); // remove callback - } - lua_closethread(thread, nullptr); - lua_pop(L, 1); // remove thread from L + (*self->cbSuccess)(HTTPResponse(res)); + self->cbSuccess = std::nullopt; }) - .onError([shared, L](const NetworkResult &res) { + .onError([L, hack](const NetworkResult &res) { + auto self = hack.lock(); + if (!self) + { + return; + } + if (!self->cbError.has_value()) + { + return; + } lua::StackGuard guard(L); - auto *thread = lua_newthread(L); - - auto priv = shared->pushPrivate(thread); - lua_getfield(thread, priv, "error"); - auto cb = lua_gettop(thread); - if (lua_isfunction(thread, cb)) - { - lua::push(thread, std::make_shared(res)); - // one arg, no return, no msgh - lua_pcall(thread, 1, 0, 0); - } - else - { - lua_pop(thread, 1); // remove callback - } - lua_closethread(thread, nullptr); - lua_pop(L, 1); // remove thread from L + (*self->cbError)(HTTPResponse(res)); + self->cbError = std::nullopt; }) - .finally([shared, L]() { + .finally([L, hack]() { + auto self = hack.lock(); + if (!self) + { + // this could happen if the plugin was deleted + return; + } + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + for (auto it = pl->httpRequests.begin(); + it < pl->httpRequests.end(); it++) + { + if (*it == self) + { + pl->httpRequests.erase(it); + break; + } + } + + if (!self->cbFinally.has_value()) + { + return; + } lua::StackGuard guard(L); - auto *thread = lua_newthread(L); - - auto priv = shared->pushPrivate(thread); - lua_getfield(thread, priv, "finally"); - auto cb = lua_gettop(thread); - if (lua_isfunction(thread, cb)) - { - // no args, no return, no msgh - lua_pcall(thread, 0, 0, 0); - } - else - { - lua_pop(thread, 1); // remove callback - } - // remove our private data - lua_pushnil(thread); - lua_setfield(thread, LUA_REGISTRYINDEX, - shared->privateKey.toStdString().c_str()); - lua_closethread(thread, nullptr); - lua_pop(L, 1); // remove thread from L - - // we removed our private table, forget the key for it - shared->privateKey = QString(); + (*self->cbFinally)(); + self->cbFinally = std::nullopt; }) .timeout(this->timeout_) .execute(); - return 0; } HTTPRequest::HTTPRequest(HTTPRequest::ConstructorAccessTag /*ignored*/, @@ -418,34 +189,10 @@ HTTPRequest::~HTTPRequest() // but that's better than accessing a possibly invalid lua_State pointer. } -StackIdx HTTPRequest::pushPrivate(lua_State *L) +QString HTTPRequest::to_string() { - if (this->privateKey.isEmpty()) - { - this->privateKey = QString("HTTPRequestPrivate%1") - .arg(QRandomGenerator::system()->generate()); - pushEmptyTable(L, 4); - lua_setfield(L, LUA_REGISTRYINDEX, - this->privateKey.toStdString().c_str()); - } - lua_getfield(L, LUA_REGISTRYINDEX, this->privateKey.toStdString().c_str()); - return lua_gettop(L); + return ""; } -// NOLINTEND(*vararg) } // namespace chatterino::lua::api - -namespace chatterino::lua { - -StackIdx push(lua_State *L, std::shared_ptr request) -{ - using namespace chatterino::lua::api; - - SharedPtrUserData::create( - L, std::move(request)); - luaL_getmetatable(L, "c2.HTTPRequest"); - lua_setmetatable(L, -2); - return lua_gettop(L); -} -} // namespace chatterino::lua #endif diff --git a/src/controllers/plugins/api/HTTPRequest.hpp b/src/controllers/plugins/api/HTTPRequest.hpp index 955a3cd2d..6fe3b97be 100644 --- a/src/controllers/plugins/api/HTTPRequest.hpp +++ b/src/controllers/plugins/api/HTTPRequest.hpp @@ -2,10 +2,16 @@ #ifdef CHATTERINO_HAVE_PLUGINS # include "common/network/NetworkRequest.hpp" # include "controllers/plugins/LuaUtilities.hpp" -# include "controllers/plugins/PluginController.hpp" + +# include +# include # include +namespace chatterino { +class PluginController; +} // namespace chatterino + namespace chatterino::lua::api { // NOLINTBEGIN(readability-identifier-naming) @@ -33,33 +39,19 @@ public: private: NetworkRequest req_; - static void createMetatable(lua_State *L); + static void createUserType(sol::table &c2); friend class chatterino::PluginController; - /** - * @brief Get the content of the top object on Lua stack, usually the first argument as an HTTPRequest - * - * If the object given is not a userdatum or the pointer inside that - * userdatum doesn't point to a HTTPRequest, a lua error is thrown. - * - * This function always returns a non-null pointer. - */ - static std::shared_ptr getOrError(lua_State *L, - StackIdx where = -1); - /** - * Pushes the private table onto the lua stack. - * - * This might create it if it doesn't exist. - */ - StackIdx pushPrivate(lua_State *L); - // This is the key in the registry the private table it held at (if it exists) // This might be a null QString if the request has already been executed or // the table wasn't created yet. - QString privateKey; int timeout_ = 10'000; bool done = false; + std::optional cbSuccess; + std::optional cbError; + std::optional cbFinally; + public: // These functions are wrapped so data can be accessed more easily. When a call from Lua comes in: // - the static wrapper function is called @@ -72,8 +64,7 @@ public: * @lua@param callback HTTPCallback Function to call when the HTTP request succeeds * @exposed HTTPRequest:on_success */ - static int on_success_wrap(lua_State *L); - int on_success(lua_State *L); + void on_success(sol::protected_function func); /** * Sets the failure callback @@ -81,8 +72,7 @@ public: * @lua@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status * @exposed HTTPRequest:on_error */ - static int on_error_wrap(lua_State *L); - int on_error(lua_State *L); + void on_error(sol::protected_function func); /** * Sets the finally callback @@ -90,8 +80,7 @@ public: * @lua@param callback fun(): nil Function to call when the HTTP request finishes * @exposed HTTPRequest:finally */ - static int finally_wrap(lua_State *L); - int finally(lua_State *L); + void finally(sol::protected_function func); /** * Sets the timeout @@ -99,8 +88,7 @@ public: * @lua@param timeout integer How long in milliseconds until the times out * @exposed HTTPRequest:set_timeout */ - static int set_timeout_wrap(lua_State *L); - int set_timeout(lua_State *L); + void set_timeout(int timeout); /** * Sets the request payload @@ -108,8 +96,7 @@ public: * @lua@param data string * @exposed HTTPRequest:set_payload */ - static int set_payload_wrap(lua_State *L); - int set_payload(lua_State *L); + void set_payload(QByteArray payload); /** * Sets a header in the request @@ -118,16 +105,19 @@ public: * @lua@param value string * @exposed HTTPRequest:set_header */ - static int set_header_wrap(lua_State *L); - int set_header(lua_State *L); + void set_header(QByteArray name, QByteArray value); /** * Executes the HTTP request * * @exposed HTTPRequest:execute */ - static int execute_wrap(lua_State *L); - int execute(lua_State *L); + void execute(sol::this_state L); + /** + * @lua@return string + * @exposed HTTPRequest:__tostring + */ + QString to_string(); /** * Static functions @@ -142,7 +132,9 @@ public: * @lua@return HTTPRequest * @exposed HTTPRequest.create */ - static int create(lua_State *L); + static std::shared_ptr create(sol::this_state L, + NetworkRequestType method, + QString url); }; // NOLINTEND(readability-identifier-naming) diff --git a/src/controllers/plugins/api/HTTPResponse.cpp b/src/controllers/plugins/api/HTTPResponse.cpp index f6d6ea1df..e18d7bf1f 100644 --- a/src/controllers/plugins/api/HTTPResponse.cpp +++ b/src/controllers/plugins/api/HTTPResponse.cpp @@ -2,77 +2,28 @@ # include "controllers/plugins/api/HTTPResponse.hpp" # include "common/network/NetworkResult.hpp" -# include "controllers/plugins/LuaAPI.hpp" +# include "controllers/plugins/SolTypes.hpp" # include "util/DebugCount.hpp" -extern "C" { # include -} +# include +# include + # include namespace chatterino::lua::api { -// NOLINTBEGIN(*vararg) -// NOLINTNEXTLINE(*-avoid-c-arrays) -static const luaL_Reg HTTP_RESPONSE_METHODS[] = { - {"data", &HTTPResponse::data_wrap}, - {"status", &HTTPResponse::status_wrap}, - {"error", &HTTPResponse::error_wrap}, - {nullptr, nullptr}, -}; -void HTTPResponse::createMetatable(lua_State *L) +void HTTPResponse::createUserType(sol::table &c2) { - lua::StackGuard guard(L, 1); + c2.new_usertype( // + "HTTPResponse", sol::no_constructor, + // metamethods + sol::meta_method::to_string, &HTTPResponse::to_string, // - luaL_newmetatable(L, "c2.HTTPResponse"); - lua_pushstring(L, "__index"); - lua_pushvalue(L, -2); // clone metatable - lua_settable(L, -3); // metatable.__index = metatable - - // Generic ISharedResource stuff - lua_pushstring(L, "__gc"); - lua_pushcfunction(L, (&SharedPtrUserData::destroy)); - lua_settable(L, -3); // metatable.__gc = SharedPtrUserData<...>::destroy - - luaL_setfuncs(L, HTTP_RESPONSE_METHODS, 0); -} - -std::shared_ptr HTTPResponse::getOrError(lua_State *L, - StackIdx where) -{ - if (lua_gettop(L) < 1) - { - // The nullptr is there just to appease the compiler, luaL_error is no return - luaL_error(L, "Called c2.HTTPResponse method without a request object"); - return nullptr; - } - if (lua_isuserdata(L, where) == 0) - { - luaL_error(L, "Called c2.HTTPResponse method with a non-userdata " - "'self' argument"); - return nullptr; - } - // luaL_checkudata is no-return if check fails - auto *checked = luaL_checkudata(L, where, "c2.HTTPResponse"); - auto *data = - SharedPtrUserData::from( - checked); - if (data == nullptr) - { - luaL_error(L, "Called c2.HTTPResponse method with an invalid pointer"); - return nullptr; - } - lua_remove(L, where); - if (data->target == nullptr) - { - luaL_error( - L, - "Internal error: SharedPtrUserData::target was null. This is a Chatterino bug!"); - return nullptr; - } - return data->target; + "data", &HTTPResponse::data, // + "status", &HTTPResponse::status, // + "error", &HTTPResponse::error // + ); } HTTPResponse::HTTPResponse(NetworkResult res) @@ -85,60 +36,30 @@ HTTPResponse::~HTTPResponse() DebugCount::decrease("lua::api::HTTPResponse"); } -int HTTPResponse::data_wrap(lua_State *L) +QByteArray HTTPResponse::data() { - lua::StackGuard guard(L, 0); // 1 in, 1 out - auto ptr = HTTPResponse::getOrError(L, 1); - return ptr->data(L); + return this->result_.getData(); } -int HTTPResponse::data(lua_State *L) +std::optional HTTPResponse::status() { - lua::push(L, this->result_.getData().toStdString()); - return 1; + return this->result_.status(); } -int HTTPResponse::status_wrap(lua_State *L) +QString HTTPResponse::error() { - lua::StackGuard guard(L, 0); // 1 in, 1 out - auto ptr = HTTPResponse::getOrError(L, 1); - return ptr->status(L); + return this->result_.formatError(); } -int HTTPResponse::status(lua_State *L) +QString HTTPResponse::to_string() { - lua::push(L, this->result_.status()); - return 1; + if (this->status().has_value()) + { + return QStringView(u"") + .arg(QString::number(*this->status())); + } + return ""; } -int HTTPResponse::error_wrap(lua_State *L) -{ - lua::StackGuard guard(L, 0); // 1 in, 1 out - auto ptr = HTTPResponse::getOrError(L, 1); - return ptr->error(L); -} - -int HTTPResponse::error(lua_State *L) -{ - lua::push(L, this->result_.formatError()); - return 1; -} - -// NOLINTEND(*vararg) } // namespace chatterino::lua::api - -namespace chatterino::lua { -StackIdx push(lua_State *L, std::shared_ptr request) -{ - using namespace chatterino::lua::api; - - // Prepare table - SharedPtrUserData::create( - L, std::move(request)); - luaL_getmetatable(L, "c2.HTTPResponse"); - lua_setmetatable(L, -2); - - return lua_gettop(L); -} -} // namespace chatterino::lua #endif diff --git a/src/controllers/plugins/api/HTTPResponse.hpp b/src/controllers/plugins/api/HTTPResponse.hpp index 205aae01e..80eb49bd3 100644 --- a/src/controllers/plugins/api/HTTPResponse.hpp +++ b/src/controllers/plugins/api/HTTPResponse.hpp @@ -1,12 +1,11 @@ #pragma once #ifdef CHATTERINO_HAVE_PLUGINS # include "common/network/NetworkResult.hpp" -# include "controllers/plugins/LuaUtilities.hpp" + +# include +# include # include -extern "C" { -# include -} namespace chatterino { class PluginController; @@ -18,7 +17,7 @@ namespace chatterino::lua::api { /** * @lua@class HTTPResponse */ -class HTTPResponse : public std::enable_shared_from_this +class HTTPResponse { NetworkResult result_; @@ -31,20 +30,9 @@ public: ~HTTPResponse(); private: - static void createMetatable(lua_State *L); + static void createUserType(sol::table &c2); friend class chatterino::PluginController; - /** - * @brief Get the content of the top object on Lua stack, usually the first argument as an HTTPResponse - * - * If the object given is not a userdatum or the pointer inside that - * userdatum doesn't point to a HTTPResponse, a lua error is thrown. - * - * This function always returns a non-null pointer. - */ - static std::shared_ptr getOrError(lua_State *L, - StackIdx where = -1); - public: /** * Returns the data. This is not guaranteed to be encoded using any @@ -52,29 +40,28 @@ public: * * @exposed HTTPResponse:data */ - static int data_wrap(lua_State *L); - int data(lua_State *L); + QByteArray data(); /** * Returns the status code. * * @exposed HTTPResponse:status */ - static int status_wrap(lua_State *L); - int status(lua_State *L); + std::optional status(); /** * A somewhat human readable description of an error if such happened * @exposed HTTPResponse:error */ + QString error(); - static int error_wrap(lua_State *L); - int error(lua_State *L); + /** + * @lua@return string + * @exposed HTTPResponse:__tostring + */ + QString to_string(); }; // NOLINTEND(readability-identifier-naming) } // namespace chatterino::lua::api -namespace chatterino::lua { -StackIdx push(lua_State *L, std::shared_ptr request); -} // namespace chatterino::lua #endif diff --git a/src/controllers/plugins/api/IOWrapper.cpp b/src/controllers/plugins/api/IOWrapper.cpp index f6a58a0bb..b3e4103b9 100644 --- a/src/controllers/plugins/api/IOWrapper.cpp +++ b/src/controllers/plugins/api/IOWrapper.cpp @@ -2,15 +2,17 @@ # include "controllers/plugins/api/IOWrapper.hpp" # include "Application.hpp" -# include "controllers/plugins/LuaUtilities.hpp" +# include "common/QLogging.hpp" # include "controllers/plugins/PluginController.hpp" -extern "C" { # include # include -} +# include +# include # include +# include +# include namespace chatterino::lua::api { @@ -91,45 +93,28 @@ struct LuaFileMode { } }; -int ioError(lua_State *L, const QString &value, int errnoequiv) +sol::variadic_results ioError(lua_State *L, const QString &value, + int errnoequiv) { - lua_pushnil(L); - lua::push(L, value); - lua::push(L, errnoequiv); - return 3; + sol::variadic_results out; + out.push_back(sol::nil); + out.push_back(sol::make_object(L, value.toStdString())); + out.push_back({L, sol::in_place_type, errnoequiv}); + return out; } -// NOLINTBEGIN(*vararg) -int io_open(lua_State *L) +sol::variadic_results io_open(sol::this_state L, QString filename, + QString strmode) { auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); if (pl == nullptr) { - luaL_error(L, "internal error: no plugin"); - return 0; + throw std::runtime_error("internal error: no plugin"); } - LuaFileMode mode; - if (lua_gettop(L) == 2) + LuaFileMode mode(strmode); + if (!mode.error.isEmpty()) { - // we have a mode - QString smode; - if (!lua::pop(L, &smode)) - { - return luaL_error( - L, - "io.open mode (2nd argument) must be a string or not present"); - } - mode = LuaFileMode(smode); - if (!mode.error.isEmpty()) - { - return luaL_error(L, mode.error.toStdString().c_str()); - } - } - QString filename; - if (!lua::pop(L, &filename)) - { - return luaL_error(L, - "io.open filename (1st argument) must be a string"); + throw std::runtime_error(mode.error.toStdString()); } QFileInfo file(pl->dataDirectory().filePath(filename)); auto abs = file.absoluteFilePath(); @@ -144,39 +129,35 @@ int io_open(lua_State *L) "Plugin does not have permissions to access given file.", EACCES); } - lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); - lua_getfield(L, -1, "open"); - lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] - lua::push(L, abs); - lua::push(L, mode.toString()); - lua_call(L, 2, 3); - return 3; + + sol::state_view lua(L); + auto open = lua.registry()[REG_REAL_IO_NAME]["open"]; + sol::protected_function_result res = + open(abs.toStdString(), mode.toString().toStdString()); + return res; +} +sol::variadic_results io_open_modeless(sol::this_state L, QString filename) +{ + return io_open(L, std::move(filename), "r"); } -int io_lines(lua_State *L) +sol::variadic_results io_lines_noargs(sol::this_state L) +{ + sol::state_view lua(L); + auto lines = lua.registry()[REG_REAL_IO_NAME]["lines"]; + sol::protected_function_result res = lines(); + return res; +} + +sol::variadic_results io_lines(sol::this_state L, QString filename, + sol::variadic_args args) { auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); if (pl == nullptr) { - luaL_error(L, "internal error: no plugin"); - return 0; - } - if (lua_gettop(L) == 0) - { - // io.lines() case, just call realio.lines - lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); - lua_getfield(L, -1, "lines"); - lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] - lua_call(L, 0, 1); - return 1; - } - QString filename; - if (!lua::pop(L, &filename)) - { - return luaL_error( - L, - "io.lines filename (1st argument) must be a string or not present"); + throw std::runtime_error("internal error: no plugin"); } + sol::state_view lua(L); QFileInfo file(pl->dataDirectory().filePath(filename)); auto abs = file.absoluteFilePath(); qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name @@ -185,191 +166,168 @@ int io_lines(lua_State *L) bool ok = pl->hasFSPermissionFor(false, abs); if (!ok) { - return ioError(L, - "Plugin does not have permissions to access given file.", - EACCES); + throw std::runtime_error( + "Plugin does not have permissions to access given file."); } - // Our stack looks like this: - // - {...}[1] - // - {...}[2] - // ... - // We want: - // - REG[REG_REAL_IO_NAME].lines - // - absolute file path - // - {...}[1] - // - {...}[2] - // ... - lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); - lua_getfield(L, -1, "lines"); - lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] - lua_insert(L, 1); // move function to start of stack - lua::push(L, abs); - lua_insert(L, 2); // move file name just after the function - lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); - return lua_gettop(L); + auto lines = lua.registry()[REG_REAL_IO_NAME]["lines"]; + sol::protected_function_result res = lines(abs.toStdString(), args); + return res; } -namespace { - - // This is the code for both io.input and io.output - int globalFileCommon(lua_State *L, bool output) +sol::variadic_results io_input_argless(sol::this_state L) +{ + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) { - auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); - if (pl == nullptr) - { - luaL_error(L, "internal error: no plugin"); - return 0; - } - // Three signature cases: - // io.input() - // io.input(file) - // io.input(name) - if (lua_gettop(L) == 0) - { - // We have no arguments, call realio.input() - lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); - if (output) - { - lua_getfield(L, -1, "output"); - } - else - { - lua_getfield(L, -1, "input"); - } - lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] - lua_call(L, 0, 1); - return 1; - } - if (lua_gettop(L) != 1) - { - return luaL_error(L, "Too many arguments given to io.input()."); - } - // Now check if we have a file or name - auto *p = luaL_testudata(L, 1, LUA_FILEHANDLE); - if (p == nullptr) - { - // this is not a file handle, send it to open - luaL_getsubtable(L, LUA_REGISTRYINDEX, REG_C2_IO_NAME); - lua_getfield(L, -1, "open"); - lua_remove(L, -2); // remove io - - lua_pushvalue(L, 1); // dupe arg - if (output) - { - lua_pushstring(L, "w"); - } - else - { - lua_pushstring(L, "r"); - } - lua_call(L, 2, 1); // call ourio.open(arg1, 'r'|'w') - // if this isn't a string ourio.open errors - - // this leaves us with: - // 1. arg - // 2. new_file - lua_remove(L, 1); // remove arg, replacing it with new_file - } - - // file handle, pass it off to realio.input - lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); - if (output) - { - lua_getfield(L, -1, "output"); - } - else - { - lua_getfield(L, -1, "input"); - } - lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] - lua_pushvalue(L, 1); // duplicate arg - lua_call(L, 1, 1); - return 1; + throw std::runtime_error("internal error: no plugin"); } + sol::state_view lua(L); -} // namespace - -int io_input(lua_State *L) -{ - return globalFileCommon(L, false); + auto func = lua.registry()[REG_REAL_IO_NAME]["input"]; + sol::protected_function_result res = func(); + return res; } - -int io_output(lua_State *L) +sol::variadic_results io_input_file(sol::this_state L, sol::userdata file) { - return globalFileCommon(L, true); -} - -int io_close(lua_State *L) -{ - if (lua_gettop(L) > 1) + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) { - return luaL_error( - L, "Too many arguments for io.close. Expected one or zero."); + throw std::runtime_error("internal error: no plugin"); } - if (lua_gettop(L) == 0) + sol::state_view lua(L); + + auto func = lua.registry()[REG_REAL_IO_NAME]["input"]; + sol::protected_function_result res = func(file); + return res; +} +sol::variadic_results io_input_name(sol::this_state L, QString filename) +{ + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) { - lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output"); + throw std::runtime_error("internal error: no plugin"); } - lua_getfield(L, -1, "close"); - lua_pushvalue(L, -2); - lua_call(L, 1, 0); - return 0; -} - -int io_flush(lua_State *L) -{ - if (lua_gettop(L) > 1) + sol::state_view lua(L); + auto res = io_open(L, std::move(filename), "r"); + if (res.size() != 1) { - return luaL_error( - L, "Too many arguments for io.flush. Expected one or zero."); + throw std::runtime_error(res.at(1).as()); } - lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output"); - lua_getfield(L, -1, "flush"); - lua_pushvalue(L, -2); - lua_call(L, 1, 0); - return 0; -} - -int io_read(lua_State *L) -{ - if (lua_gettop(L) > 1) + auto obj = res.at(0); + if (obj.get_type() != sol::type::userdata) { - return luaL_error( - L, "Too many arguments for io.read. Expected one or zero."); + throw std::runtime_error("a file must be a userdata."); } - lua_getfield(L, LUA_REGISTRYINDEX, "_IO_input"); - lua_getfield(L, -1, "read"); - lua_insert(L, 1); - lua_insert(L, 2); - lua_call(L, lua_gettop(L) - 1, 1); - return 1; + return io_input_file(L, obj); } -int io_write(lua_State *L) +sol::variadic_results io_output_argless(sol::this_state L) { - lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output"); - lua_getfield(L, -1, "write"); - lua_insert(L, 1); - lua_insert(L, 2); - // (input) - // (input).read - // args - lua_call(L, lua_gettop(L) - 1, 1); - return 1; -} + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + throw std::runtime_error("internal error: no plugin"); + } + sol::state_view lua(L); -int io_popen(lua_State *L) + auto func = lua.registry()[REG_REAL_IO_NAME]["output"]; + sol::protected_function_result res = func(); + return res; +} +sol::variadic_results io_output_file(sol::this_state L, sol::userdata file) { - return luaL_error(L, "io.popen: This function is a stub!"); -} + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + throw std::runtime_error("internal error: no plugin"); + } + sol::state_view lua(L); -int io_tmpfile(lua_State *L) + auto func = lua.registry()[REG_REAL_IO_NAME]["output"]; + sol::protected_function_result res = func(file); + return res; +} +sol::variadic_results io_output_name(sol::this_state L, QString filename) { - return luaL_error(L, "io.tmpfile: This function is a stub!"); + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + throw std::runtime_error("internal error: no plugin"); + } + sol::state_view lua(L); + auto res = io_open(L, std::move(filename), "w"); + if (res.size() != 1) + { + throw std::runtime_error(res.at(1).as()); + } + auto obj = res.at(0); + if (obj.get_type() != sol::type::userdata) + { + throw std::runtime_error("internal error: a file must be a userdata."); + } + return io_output_file(L, obj); } -// NOLINTEND(*vararg) +bool io_close_argless(sol::this_state L) +{ + sol::state_view lua(L); + auto out = lua.registry()["_IO_output"]; + return io_close_file(L, out); +} + +bool io_close_file(sol::this_state L, sol::userdata file) +{ + sol::state_view lua(L); + return file["close"](file); +} + +void io_flush_argless(sol::this_state L) +{ + sol::state_view lua(L); + auto out = lua.registry()["_IO_output"]; + io_flush_file(L, out); +} + +void io_flush_file(sol::this_state L, sol::userdata file) +{ + sol::state_view lua(L); + file["flush"](file); +} + +sol::variadic_results io_read(sol::this_state L, sol::variadic_args args) +{ + sol::state_view lua(L); + auto inp = lua.registry()["_IO_input"]; + if (!inp.is()) + { + throw std::runtime_error("Input not set to a file"); + } + sol::protected_function read = inp["read"]; + return read(inp, args); +} + +sol::variadic_results io_write(sol::this_state L, sol::variadic_args args) +{ + sol::state_view lua(L); + auto out = lua.registry()["_IO_output"]; + if (!out.is()) + { + throw std::runtime_error("Output not set to a file"); + } + sol::protected_function write = out["write"]; + return write(out, args); +} + +void io_popen() +{ + throw std::runtime_error("io.popen: This function is a stub!"); +} + +void io_tmpfile() +{ + throw std::runtime_error("io.tmpfile: This function is a stub!"); +} } // namespace chatterino::lua::api #endif diff --git a/src/controllers/plugins/api/IOWrapper.hpp b/src/controllers/plugins/api/IOWrapper.hpp index 24ee2801e..16b1ae178 100644 --- a/src/controllers/plugins/api/IOWrapper.hpp +++ b/src/controllers/plugins/api/IOWrapper.hpp @@ -1,5 +1,9 @@ #pragma once #ifdef CHATTERINO_HAVE_PLUGINS +# include +# include +# include +# include struct lua_State; @@ -8,7 +12,6 @@ namespace chatterino::lua::api { // These functions are exposed as `_G.io`, they are wrappers for native Lua functionality. const char *const REG_REAL_IO_NAME = "real_lua_io_lib"; -const char *const REG_C2_IO_NAME = "c2io"; /** * Opens a file. @@ -20,7 +23,9 @@ const char *const REG_C2_IO_NAME = "c2io"; * @lua@param mode nil|"r"|"w"|"a"|"r+"|"w+"|"a+" * @exposed io.open */ -int io_open(lua_State *L); +sol::variadic_results io_open(sol::this_state L, QString filename, + QString strmode); +sol::variadic_results io_open_modeless(sol::this_state L, QString filename); /** * Equivalent to io.input():lines("l") or a specific iterator over given file @@ -32,7 +37,9 @@ int io_open(lua_State *L); * @lua@param ... * @exposed io.lines */ -int io_lines(lua_State *L); +sol::variadic_results io_lines(sol::this_state L, QString filename, + sol::variadic_args args); +sol::variadic_results io_lines_noargs(sol::this_state L); /** * Opens a file and sets it as default input or if given no arguments returns the default input. @@ -42,7 +49,9 @@ int io_lines(lua_State *L); * @lua@return nil|FILE* * @exposed io.input */ -int io_input(lua_State *L); +sol::variadic_results io_input_argless(sol::this_state L); +sol::variadic_results io_input_file(sol::this_state L, sol::userdata file); +sol::variadic_results io_input_name(sol::this_state L, QString filename); /** * Opens a file and sets it as default output or if given no arguments returns the default output @@ -52,7 +61,9 @@ int io_input(lua_State *L); * @lua@return nil|FILE* * @exposed io.output */ -int io_output(lua_State *L); +sol::variadic_results io_output_argless(sol::this_state L); +sol::variadic_results io_output_file(sol::this_state L, sol::userdata file); +sol::variadic_results io_output_name(sol::this_state L, QString filename); /** * Closes given file or io.output() if not given. @@ -61,7 +72,8 @@ int io_output(lua_State *L); * @lua@param nil|FILE* * @exposed io.close */ -int io_close(lua_State *L); +bool io_close_argless(sol::this_state L); +bool io_close_file(sol::this_state L, sol::userdata file); /** * Flushes the buffer for given file or io.output() if not given. @@ -70,7 +82,8 @@ int io_close(lua_State *L); * @lua@param nil|FILE* * @exposed io.flush */ -int io_flush(lua_State *L); +void io_flush_argless(sol::this_state L); +void io_flush_file(sol::this_state L, sol::userdata file); /** * Reads some data from the default input file @@ -79,7 +92,7 @@ int io_flush(lua_State *L); * @lua@param nil|string * @exposed io.read */ -int io_read(lua_State *L); +sol::variadic_results io_read(sol::this_state L, sol::variadic_args args); /** * Writes some data to the default output file @@ -88,10 +101,10 @@ int io_read(lua_State *L); * @lua@param nil|string * @exposed io.write */ -int io_write(lua_State *L); +sol::variadic_results io_write(sol::this_state L, sol::variadic_args args); -int io_popen(lua_State *L); -int io_tmpfile(lua_State *L); +void io_popen(); +void io_tmpfile(); // NOLINTEND(readability-identifier-naming) } // namespace chatterino::lua::api diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 41eb53de8..484b52bbb 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -64,22 +64,55 @@ const int MAX_QUEUED_REDEMPTIONS = 16; class TwitchChannel final : public Channel, public ChannelChatters { public: + /** + * @lua@class StreamStatus + */ struct StreamStatus { + /** + * @lua@field live boolean + */ bool live = false; bool rerun = false; + /** + * @lua@field viewer_count number + */ unsigned viewerCount = 0; + /** + * @lua@field title string Stream title or last stream title + */ QString title; + /** + * @lua@field game_name string + */ QString game; + /** + * @lua@field game_id string + */ QString gameId; QString uptime; + /** + * @lua@field uptime number Seconds since the stream started. + */ int uptimeSeconds = 0; QString streamType; QString streamId; }; + /** + * @lua@class RoomModes + */ struct RoomModes { + /** + * @lua@field subscriber_only boolean + */ bool submode = false; + /** + * @lua@field unique_chat boolean You might know this as r9kbeta or robot9000. + */ bool r9k = false; + /** + * @lua@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes + */ bool emoteOnly = false; /** @@ -88,6 +121,8 @@ public: * Special cases: * -1 = follower mode off * 0 = follower mode on, no time requirement + * + * @lua@field follower_only number? Time in minutes you need to follow to chat or nil. **/ int followerOnly = -1; @@ -95,6 +130,8 @@ public: * @brief Number of seconds required to wait before typing emotes * * 0 = slow mode off + * + * @lua@field slow_mode number? Time in seconds you need to wait before sending messages or nil. **/ int slowMode = 0; }; diff --git a/src/util/TypeName.hpp b/src/util/TypeName.hpp index 3e5c674e8..1f6066381 100644 --- a/src/util/TypeName.hpp +++ b/src/util/TypeName.hpp @@ -28,6 +28,15 @@ constexpr auto type_name() name.remove_prefix(prefix.size()); name.remove_suffix(suffix.size()); + if (name.starts_with("class ")) + { + name.remove_prefix(6); + } + if (name.starts_with("struct ")) + { + name.remove_prefix(7); + } + return name; } diff --git a/src/widgets/splits/SplitContainer.cpp b/src/widgets/splits/SplitContainer.cpp index c2ddc1160..1cca478f0 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -805,6 +805,32 @@ void SplitContainer::popup() window.show(); } +QString channelTypeToString(Channel::Type value) noexcept +{ + using Type = chatterino::Channel::Type; + switch (value) + { + default: + assert(false && "value cannot be serialized"); + return "never"; + + case Type::Twitch: + return "twitch"; + case Type::TwitchWhispers: + return "whispers"; + case Type::TwitchWatching: + return "watching"; + case Type::TwitchMentions: + return "mentions"; + case Type::TwitchLive: + return "live"; + case Type::TwitchAutomod: + return "automod"; + case Type::Misc: + return "misc"; + } +} + NodeDescriptor SplitContainer::buildDescriptorRecursively( const Node *currentNode) const { @@ -814,7 +840,7 @@ NodeDescriptor SplitContainer::buildDescriptorRecursively( currentNode->split_->getIndirectChannel().getType(); SplitNodeDescriptor result; - result.type_ = qmagicenum::enumNameString(channelType); + result.type_ = channelTypeToString(channelType); result.channelName_ = currentNode->split_->getChannel()->getName(); result.filters_ = currentNode->split_->getFilters(); return result; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5a2cb5f1b..547f0e7c0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,6 +48,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp ${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.cpp ${CMAKE_CURRENT_LIST_DIR}/src/CancellationToken.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/Plugins.cpp ${CMAKE_CURRENT_LIST_DIR}/src/TwitchIrc.cpp ${CMAKE_CURRENT_LIST_DIR}/src/IgnoreController.cpp ${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp diff --git a/tests/src/NetworkHelpers.hpp b/tests/src/NetworkHelpers.hpp new file mode 100644 index 000000000..f55355e4a --- /dev/null +++ b/tests/src/NetworkHelpers.hpp @@ -0,0 +1,55 @@ +#pragma once +#include "Test.hpp" + +#include +namespace chatterino { + +#ifdef CHATTERINO_TEST_USE_PUBLIC_HTTPBIN +// Using our self-hosted version of httpbox https://github.com/kevinastone/httpbox +const char *const HTTPBIN_BASE_URL = "https://braize.pajlada.com/httpbox"; +#else +const char *const HTTPBIN_BASE_URL = "http://127.0.0.1:9051"; +#endif + +class RequestWaiter +{ +public: + void requestDone() + { + { + std::unique_lock lck(this->mutex_); + ASSERT_FALSE(this->requestDone_); + this->requestDone_ = true; + } + this->condition_.notify_one(); + } + + void waitForRequest() + { + using namespace std::chrono_literals; + + while (true) + { + { + std::unique_lock lck(this->mutex_); + bool done = this->condition_.wait_for(lck, 10ms, [this] { + return this->requestDone_; + }); + if (done) + { + break; + } + } + QCoreApplication::processEvents(QEventLoop::AllEvents); + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + } + + ASSERT_TRUE(this->requestDone_); + } + +private: + std::mutex mutex_; + std::condition_variable condition_; + bool requestDone_ = false; +}; +} // namespace chatterino diff --git a/tests/src/NetworkRequest.cpp b/tests/src/NetworkRequest.cpp index ca723481e..44b7504c7 100644 --- a/tests/src/NetworkRequest.cpp +++ b/tests/src/NetworkRequest.cpp @@ -2,6 +2,7 @@ #include "common/network/NetworkManager.hpp" #include "common/network/NetworkResult.hpp" +#include "NetworkHelpers.hpp" #include "Test.hpp" #include @@ -10,14 +11,6 @@ using namespace chatterino; namespace { -#ifdef CHATTERINO_TEST_USE_PUBLIC_HTTPBIN -// Not using httpbin.org, since it can be really slow and cause timeouts. -// postman-echo has the same API. -const char *const HTTPBIN_BASE_URL = "https://postman-echo.com"; -#else -const char *const HTTPBIN_BASE_URL = "http://127.0.0.1:9051"; -#endif - QString getStatusURL(int code) { return QString("%1/status/%2").arg(HTTPBIN_BASE_URL).arg(code); @@ -28,46 +21,6 @@ QString getDelayURL(int delay) return QString("%1/delay/%2").arg(HTTPBIN_BASE_URL).arg(delay); } -class RequestWaiter -{ -public: - void requestDone() - { - { - std::unique_lock lck(this->mutex_); - ASSERT_FALSE(this->requestDone_); - this->requestDone_ = true; - } - this->condition_.notify_one(); - } - - void waitForRequest() - { - using namespace std::chrono_literals; - - while (true) - { - { - std::unique_lock lck(this->mutex_); - bool done = this->condition_.wait_for(lck, 10ms, [this] { - return this->requestDone_; - }); - if (done) - { - break; - } - } - QCoreApplication::processEvents(QEventLoop::AllEvents); - QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - } - } - -private: - std::mutex mutex_; - std::condition_variable condition_; - bool requestDone_ = false; -}; - } // namespace TEST(NetworkRequest, Success) diff --git a/tests/src/Plugins.cpp b/tests/src/Plugins.cpp new file mode 100644 index 000000000..588d3c2ff --- /dev/null +++ b/tests/src/Plugins.cpp @@ -0,0 +1,641 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "Application.hpp" +# include "common/Channel.hpp" +# include "common/network/NetworkCommon.hpp" +# include "controllers/commands/Command.hpp" // IWYU pragma: keep +# include "controllers/commands/CommandController.hpp" +# include "controllers/plugins/api/ChannelRef.hpp" +# include "controllers/plugins/Plugin.hpp" +# include "controllers/plugins/PluginController.hpp" +# include "controllers/plugins/PluginPermission.hpp" +# include "controllers/plugins/SolTypes.hpp" // IWYU pragma: keep +# include "mocks/BaseApplication.hpp" +# include "mocks/Channel.hpp" +# include "mocks/Emotes.hpp" +# include "mocks/Logging.hpp" +# include "mocks/TwitchIrcServer.hpp" +# include "NetworkHelpers.hpp" +# include "singletons/Logging.hpp" +# include "Test.hpp" + +# include +# include +# include + +# include +# include +# include + +using namespace chatterino; +using chatterino::mock::MockChannel; + +namespace { + +const QString TEST_SETTINGS = R"( +{ + "plugins": { + "supportEnabled": true, + "enabledPlugins": [ + "test" + ] + } +} +)"; + +class MockTwitch : public mock::MockTwitchIrcServer +{ +public: + ChannelPtr mm2pl = std::make_shared("mm2pl"); + + ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) override + { + if (dirtyChannelName == "mm2pl") + { + return this->mm2pl; + } + return Channel::getEmpty(); + } + + std::shared_ptr getChannelOrEmptyByID( + const QString &channelID) override + { + if (channelID == "117691339") + { + return this->mm2pl; + } + return Channel::getEmpty(); + } +}; + +class MockApplication : public mock::BaseApplication +{ +public: + MockApplication() + : mock::BaseApplication(TEST_SETTINGS) + , plugins(this->paths_) + , commands(this->paths_) + { + } + + PluginController *getPlugins() override + { + return &this->plugins; + } + + CommandController *getCommands() override + { + return &this->commands; + } + + IEmotes *getEmotes() override + { + return &this->emotes; + } + + mock::MockTwitchIrcServer *getTwitch() override + { + return &this->twitch; + } + + ILogging *getChatLogger() override + { + return &this->logging; + } + + PluginController plugins; + mock::EmptyLogging logging; + CommandController commands; + mock::Emotes emotes; + MockTwitch twitch; +}; + +} // namespace + +namespace chatterino { + +class PluginControllerAccess +{ +public: + static bool tryLoadFromDir(const QDir &pluginDir) + { + return getApp()->getPlugins()->tryLoadFromDir(pluginDir); + } + + static void openLibrariesFor(Plugin *plugin) + { + return PluginController::openLibrariesFor(plugin); + } + + static std::map> &plugins() + { + return getApp()->getPlugins()->plugins_; + } + + static lua_State *state(Plugin *pl) + { + return pl->state_; + } +}; + +} // namespace chatterino + +class PluginTest : public ::testing::Test +{ +protected: + void configure(std::vector permissions = {}) + { + this->app = std::make_unique(); + + auto &plugins = PluginControllerAccess::plugins(); + { + PluginMeta meta; + meta.name = "Test"; + meta.license = "MIT"; + meta.homepage = "https://github.com/Chatterino/chatterino2"; + meta.description = "Plugin for tests"; + meta.permissions = std::move(permissions); + + QDir plugindir = + QDir(app->paths_.pluginsDirectory).absoluteFilePath("test"); + + plugindir.mkpath("."); + auto temp = std::make_unique("test", luaL_newstate(), meta, + plugindir); + this->rawpl = temp.get(); + plugins.insert({"test", std::move(temp)}); + } + + // XXX: this skips PluginController::load() + PluginControllerAccess::openLibrariesFor(rawpl); + this->lua = new sol::state_view(PluginControllerAccess::state(rawpl)); + + this->channel = app->twitch.mm2pl; + this->rawpl->dataDirectory().mkpath("."); + } + + void TearDown() override + { + // perform safe destruction of the plugin + delete this->lua; + this->lua = nullptr; + PluginControllerAccess::plugins().clear(); + this->rawpl = nullptr; + this->app.reset(); + } + + Plugin *rawpl = nullptr; + std::unique_ptr app; + sol::state_view *lua; + ChannelPtr channel; +}; + +TEST_F(PluginTest, testCommands) +{ + configure(); + + lua->script(R"lua( + _G.called = false + _G.words = nil + _G.channel = nil + c2.register_command("/test", function(ctx) + _G.called = true + _G.words = ctx.words + _G.channel = ctx.channel + end) + )lua"); + + EXPECT_EQ(app->commands.pluginCommands(), QStringList{"/test"}); + app->commands.execCommand("/test with arguments", channel, false); + bool called = (*lua)["called"]; + EXPECT_EQ(called, true); + + EXPECT_NE((*lua)["words"], sol::nil); + { + sol::table tbl = (*lua)["words"]; + std::vector words; + for (auto &o : tbl) + { + words.push_back(o.second.as()); + } + EXPECT_EQ(words, + std::vector({"/test", "with", "arguments"})); + } + + sol::object chnobj = (*lua)["channel"]; + EXPECT_EQ(chnobj.get_type(), sol::type::userdata); + lua::api::ChannelRef ref = chnobj.as(); + EXPECT_EQ(ref.get_name(), channel->getName()); +} + +TEST_F(PluginTest, testCompletion) +{ + configure(); + + lua->script(R"lua( + _G.called = false + _G.query = nil + _G.full_text_content = nil + _G.cursor_position = nil + _G.is_first_word = nil + + c2.register_callback( + c2.EventType.CompletionRequested, + function(ev) + _G.called = true + _G.query = ev.query + _G.full_text_content = ev.full_text_content + _G.cursor_position = ev.cursor_position + _G.is_first_word = ev.is_first_word + if ev.query == "exclusive" then + return { + hide_others = true, + values = {"Completion1", "Completion2"} + } + end + return { + hide_others = false, + values = {"Completion"}, + } + end + ) + )lua"); + + bool done{}; + QStringList results; + std::tie(done, results) = + app->plugins.updateCustomCompletions("foo", "foo", 3, true); + ASSERT_EQ(done, false); + ASSERT_EQ(results, QStringList{"Completion"}); + + ASSERT_EQ((*lua).get("query"), "foo"); + ASSERT_EQ((*lua).get("full_text_content"), "foo"); + ASSERT_EQ((*lua).get("cursor_position"), 3); + ASSERT_EQ((*lua).get("is_first_word"), true); + + std::tie(done, results) = app->plugins.updateCustomCompletions( + "exclusive", "foo exclusive", 13, false); + ASSERT_EQ(done, true); + ASSERT_EQ(results, QStringList({"Completion1", "Completion2"})); + + ASSERT_EQ((*lua).get("query"), "exclusive"); + ASSERT_EQ((*lua).get("full_text_content"), "foo exclusive"); + ASSERT_EQ((*lua).get("cursor_position"), 13); + ASSERT_EQ((*lua).get("is_first_word"), false); +} + +TEST_F(PluginTest, testChannel) +{ + configure(); + lua->script(R"lua( + chn = c2.Channel.by_name("mm2pl") + )lua"); + + ASSERT_EQ(lua->script(R"lua( return chn:get_name() )lua").get(0), + "mm2pl"); + ASSERT_EQ( + lua->script(R"lua( return chn:get_type() )lua").get(0), + Channel::Type::Twitch); + ASSERT_EQ( + lua->script(R"lua( return chn:get_display_name() )lua").get(0), + "mm2pl"); + // TODO: send_message, add_system_message + + ASSERT_EQ( + lua->script(R"lua( return chn:is_twitch_channel() )lua").get(0), + true); + + // this is not a TwitchChannel + const auto *shouldThrow1 = R"lua( + return chn:is_broadcaster() + )lua"; + EXPECT_ANY_THROW(lua->script(shouldThrow1)); + const auto *shouldThrow2 = R"lua( + return chn:is_mod() + )lua"; + EXPECT_ANY_THROW(lua->script(shouldThrow2)); + const auto *shouldThrow3 = R"lua( + return chn:is_vip() + )lua"; + EXPECT_ANY_THROW(lua->script(shouldThrow3)); + const auto *shouldThrow4 = R"lua( + return chn:get_twitch_id() + )lua"; + EXPECT_ANY_THROW(lua->script(shouldThrow4)); +} + +TEST_F(PluginTest, testHttp) +{ + { + PluginPermission net; + net.type = PluginPermission::Type::Network; + configure({net}); + } + + lua->script(R"lua( + function DoReq(url, postdata) + r = c2.HTTPRequest.create(method, url) + r:on_success(function(res) + status = res:status() + data = res:data() + error = res:error() + success = true + end) + r:on_error(function(res) + status = res:status() + data = res:data() + error = res:error() + failure = true + end) + r:finally(function() + finally = true + done() + end) + if postdata ~= "" then + r:set_payload(postdata) + r:set_header("Content-Type", "text/plain") + end + r:set_timeout(1000) + r:execute() + end + )lua"); + + struct RequestCase { + QString url; + bool success; + bool failure; + + int status; + QString error; + + NetworkRequestType meth = NetworkRequestType::Get; + QByteArray data; // null means do not check + }; + + std::vector cases{ + {"/status/200", true, false, 200, "200"}, + {"/delay/2", false, true, 0, "TimeoutError"}, + {"/post", true, false, 200, "200", NetworkRequestType::Post, + "Example data"}, + }; + + for (const auto &c : cases) + { + lua->script(R"lua( + success = false + failure = false + finally = false + + status = nil + data = nil + error = nil + )lua"); + RequestWaiter waiter; + (*lua)["method"] = c.meth; + (*lua)["done"] = [&waiter]() { + waiter.requestDone(); + }; + + (*lua)["DoReq"](HTTPBIN_BASE_URL + c.url, c.data); + waiter.waitForRequest(); + + EXPECT_EQ(lua->get("success"), c.success); + EXPECT_EQ(lua->get("failure"), c.failure); + EXPECT_EQ(lua->get("finally"), true); + + if (c.status != 0) + { + EXPECT_EQ(lua->get("status"), c.status); + } + else + { + EXPECT_EQ((*lua)["status"], sol::nil); + } + EXPECT_EQ(lua->get("error"), c.error); + if (!c.data.isNull()) + { + EXPECT_EQ(lua->get("data"), c.data); + } + } +} + +const QByteArray TEST_FILE_DATA = "Test file data\nWith a new line.\n"; + +TEST_F(PluginTest, ioTest) +{ + { + PluginPermission ioread; + PluginPermission iowrite; + ioread.type = PluginPermission::Type::FilesystemRead; + iowrite.type = PluginPermission::Type::FilesystemWrite; + configure({ioread, iowrite}); + } + + lua->set("TEST_DATA", TEST_FILE_DATA); + + lua->script(R"lua( + f, err = io.open("testfile", "w") + print(f, err) + f:write(TEST_DATA) + f:close() + + f, err = io.open("testfile", "r") + out = f:read("a") + f:close() + )lua"); + EXPECT_EQ(lua->get("out"), TEST_FILE_DATA); + + lua->script(R"lua( + io.input("testfile") + out = io.read("a") + )lua"); + EXPECT_EQ(lua->get("out"), TEST_FILE_DATA); + + const auto *shouldThrow1 = R"lua( + io.popen("/bin/sh", "-c", "notify-send \"This should not execute.\"") + )lua"; + EXPECT_ANY_THROW(lua->script(shouldThrow1)); + const auto *shouldThrow2 = R"lua( + io.tmpfile() + )lua"; + EXPECT_ANY_THROW(lua->script(shouldThrow2)); +} + +TEST_F(PluginTest, ioNoPerms) +{ + configure(); + auto file = rawpl->dataDirectory().filePath("testfile"); + QFile f(file); + f.open(QFile::WriteOnly); + f.write(TEST_FILE_DATA); + f.close(); + + EXPECT_EQ( + // clang-format off + lua->script(R"lua( + f, err = io.open("testfile", "r") + return err + )lua").get(0), + "Plugin does not have permissions to access given file." + // clang-format on + ); + + const auto *shouldThrow1 = R"lua( + io.input("testfile") + )lua"; + EXPECT_ANY_THROW(lua->script(shouldThrow1)); + + EXPECT_EQ( + // clang-format off + lua->script(R"lua( + f, err = io.open("testfile", "w") + return err + )lua").get(0), + "Plugin does not have permissions to access given file." + // clang-format on + ); + + const auto *shouldThrow2 = R"lua( + io.output("testfile") + )lua"; + EXPECT_ANY_THROW(lua->script(shouldThrow2)); + + const auto *shouldThrow3 = R"lua( + io.lines("testfile") + )lua"; + EXPECT_ANY_THROW(lua->script(shouldThrow3)); +} + +TEST_F(PluginTest, requireNoData) +{ + { + PluginPermission ioread; + PluginPermission iowrite; + ioread.type = PluginPermission::Type::FilesystemRead; + iowrite.type = PluginPermission::Type::FilesystemWrite; + configure({ioread, iowrite}); + } + + auto file = rawpl->dataDirectory().filePath("thisiscode.lua"); + QFile f(file); + f.open(QFile::WriteOnly); + f.write(R"lua(print("Data was executed"))lua"); + f.close(); + + const auto *shouldThrow1 = R"lua( + require("data.thisiscode") + )lua"; + EXPECT_ANY_THROW(lua->script(shouldThrow1)); +} + +TEST_F(PluginTest, testTimerRec) +{ + configure(); + + RequestWaiter waiter; + lua->set("done", [&] { + waiter.requestDone(); + }); + + sol::protected_function fn = lua->script(R"lua( + local i = 0 + f = function() + i = i + 1 + c2.log(c2.LogLevel.Info, "cb", i) + if i < 1024 then + c2.later(f, 1) + else + done() + end + end + c2.later(f, 1) + )lua"); + waiter.waitForRequest(); +} + +TEST_F(PluginTest, tryCallTest) +{ + configure(); + lua->script(R"lua( + function return_table() + return { + a="b" + } + end + function return_nothing() + end + function return_nil() + return nil + end + function return_nothing_and_error() + error("I failed :)") + end + )lua"); + + using func = sol::protected_function; + + func returnTable = lua->get("return_table"); + func returnNil = lua->get("return_nil"); + func returnNothing = lua->get("return_nothing"); + func returnNothingAndError = lua->get("return_nothing_and_error"); + + // happy paths + { + auto res = lua::tryCall(returnTable); + EXPECT_TRUE(res.has_value()); + auto t = res.value(); + EXPECT_EQ(t.get("a"), "b"); + } + { + // valid void return + auto res = lua::tryCall(returnNil); + EXPECT_TRUE(res.has_value()); + } + { + // valid void return + auto res = lua::tryCall(returnNothing); + EXPECT_TRUE(res.has_value()); + } + { + auto res = lua::tryCall(returnNothingAndError); + EXPECT_FALSE(res.has_value()); + EXPECT_EQ(res.error(), "[string \"...\"]:13: I failed :)"); + } + { + auto res = lua::tryCall>(returnNil); + EXPECT_TRUE(res.has_value()); // no error + auto opt = *res; + EXPECT_FALSE(opt.has_value()); // but also no false + } + + // unhappy paths + { + // wrong return type + auto res = lua::tryCall(returnTable); + EXPECT_FALSE(res.has_value()); + EXPECT_EQ(res.error(), + "Expected int to be returned but table was returned"); + } + { + // optional but bad return type + auto res = lua::tryCall>(returnTable); + EXPECT_FALSE(res.has_value()); + EXPECT_EQ(res.error(), "Expected std::optional to be returned but " + "table was returned"); + } + { + // no return + auto res = lua::tryCall(returnNothing); + EXPECT_FALSE(res.has_value()); + EXPECT_EQ(res.error(), + "Expected int to be returned but none was returned"); + } + { + // nil return + auto res = lua::tryCall(returnNil); + EXPECT_FALSE(res.has_value()); + EXPECT_EQ(res.error(), + "Expected int to be returned but lua_nil was returned"); + } +} + +#endif From e35fabfabe2f430e5614340a0fc6e5009db415b3 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 20 Oct 2024 12:40:48 +0200 Subject: [PATCH 22/40] refactor: irc message builder (#5663) --- CHANGELOG.md | 1 + benchmarks/CMakeLists.txt | 1 - benchmarks/src/Highlights.cpp | 102 -- src/messages/Message.hpp | 1 + src/messages/MessageBuilder.cpp | 1090 ++++++++--------- src/messages/MessageBuilder.hpp | 206 ++-- src/providers/twitch/IrcMessageHandler.cpp | 94 +- .../IrcMessageHandler/bad-emotes.json | 2 +- .../IrcMessageHandler/bad-emotes2.json | 2 +- tests/snapshots/IrcMessageHandler/emote.json | 2 +- tests/snapshots/IrcMessageHandler/emotes.json | 2 +- tests/src/Filters.cpp | 4 +- 12 files changed, 703 insertions(+), 804 deletions(-) delete mode 100644 benchmarks/src/Highlights.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae647165..1a08aac48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,7 @@ - Dev: Move plugins to Sol2. (#5622) - Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652) - Dev: Decoupled reply parsing from `MessageBuilder`. (#5660) +- Dev: Refactored IRC message building. (#5663) ## 2.5.1 diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index a0e73332e..2677365ed 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -5,7 +5,6 @@ set(benchmark_SOURCES resources/bench.qrc src/Emojis.cpp - src/Highlights.cpp src/FormatTime.cpp src/Helpers.cpp src/LimitedQueue.cpp diff --git a/benchmarks/src/Highlights.cpp b/benchmarks/src/Highlights.cpp deleted file mode 100644 index 69c69db49..000000000 --- a/benchmarks/src/Highlights.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "Application.hpp" -#include "common/Channel.hpp" -#include "controllers/accounts/AccountController.hpp" -#include "controllers/highlights/HighlightController.hpp" -#include "controllers/highlights/HighlightPhrase.hpp" -#include "messages/Message.hpp" -#include "messages/MessageBuilder.hpp" -#include "mocks/BaseApplication.hpp" -#include "mocks/UserData.hpp" -#include "util/Helpers.hpp" - -#include -#include -#include -#include - -using namespace chatterino; - -class BenchmarkMessageBuilder : public MessageBuilder -{ -public: - explicit BenchmarkMessageBuilder( - Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args) - : MessageBuilder(_channel, _ircMessage, _args) - { - } - - virtual MessagePtr build() - { - // PARSE - this->parse(); - this->usernameColor_ = getRandomColor(this->ircMessage->nick()); - - // words - // this->addWords(this->originalMessage_.split(' ')); - - this->message().messageText = this->originalMessage_; - this->message().searchText = this->message().localizedName + " " + - this->userName + ": " + - this->originalMessage_; - return nullptr; - } - - void bench() - { - this->parseHighlights(); - } -}; - -class MockApplication : public mock::BaseApplication -{ -public: - MockApplication() - : highlights(this->settings, &this->accounts) - { - } - - AccountController *getAccounts() override - { - return &this->accounts; - } - HighlightController *getHighlights() override - { - return &this->highlights; - } - - IUserDataController *getUserData() override - { - return &this->userData; - } - - AccountController accounts; - HighlightController highlights; - mock::UserDataController userData; -}; - -static void BM_HighlightTest(benchmark::State &state) -{ - MockApplication mockApplication; - - std::string message = - R"(@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=41:6-13,15-22;flags=;id=a3196c7e-be4c-4b49-9c5a-8b8302b50c2a;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922213730;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :-tags Kreygasm,Kreygasm (no space))"; - auto ircMessage = Communi::IrcMessage::fromData(message.c_str(), nullptr); - auto privMsg = dynamic_cast(ircMessage); - assert(privMsg != nullptr); - MessageParseArgs args; - auto emptyChannel = Channel::getEmpty(); - - for (auto _ : state) - { - state.PauseTiming(); - BenchmarkMessageBuilder b(emptyChannel.get(), privMsg, args); - - b.build(); - state.ResumeTiming(); - - b.bench(); - } -} - -BENCHMARK(BM_HighlightTest); diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index dd0fa26ff..19ccd60fe 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -22,6 +22,7 @@ class ScrollbarHighlight; struct Message; using MessagePtr = std::shared_ptr; +using MessagePtrMut = std::shared_ptr; struct Message { Message(); ~Message(); diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index fca5753a8..db479adf3 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -42,6 +42,7 @@ #include "util/Helpers.hpp" #include "util/IrcHelpers.hpp" #include "util/QStringHash.hpp" +#include "util/Variant.hpp" #include "widgets/Window.hpp" #include @@ -52,6 +53,7 @@ #include #include +#include #include #include @@ -148,8 +150,7 @@ QUrl getFallbackHighlightSound() } void actuallyTriggerHighlights(const QString &channelName, bool playSound, - const std::optional &customSoundUrl, - bool windowAlert) + const QUrl &customSoundUrl, bool windowAlert) { if (getApp()->getStreamerMode()->isEnabled() && getSettings()->streamerModeMuteMentions) @@ -170,13 +171,8 @@ void actuallyTriggerHighlights(const QString &channelName, bool playSound, if (playSound && resolveFocus) { - // TODO(C++23): optional or_else - QUrl soundUrl; - if (customSoundUrl) - { - soundUrl = *customSoundUrl; - } - else + QUrl soundUrl = customSoundUrl; + if (soundUrl.isEmpty()) { soundUrl = getFallbackHighlightSound(); } @@ -384,6 +380,97 @@ EmotePtr makeAutoModBadge() Url{"https://dashboard.twitch.tv/settings/moderation/automod"}}); } +std::tuple, MessageElementFlags, bool> parseEmote( + TwitchChannel *twitchChannel, const EmoteName &name) +{ + // Emote order: + // - FrankerFaceZ Channel + // - BetterTTV Channel + // - 7TV Channel + // - FrankerFaceZ Global + // - BetterTTV Global + // - 7TV Global + + const auto *globalFfzEmotes = getApp()->getFfzEmotes(); + const auto *globalBttvEmotes = getApp()->getBttvEmotes(); + const auto *globalSeventvEmotes = getApp()->getSeventvEmotes(); + + std::optional emote{}; + + if (twitchChannel != nullptr) + { + // Check for channel emotes + + emote = twitchChannel->ffzEmote(name); + if (emote) + { + return { + emote, + MessageElementFlag::FfzEmote, + false, + }; + } + + emote = twitchChannel->bttvEmote(name); + if (emote) + { + return { + emote, + MessageElementFlag::BttvEmote, + false, + }; + } + + emote = twitchChannel->seventvEmote(name); + if (emote) + { + return { + emote, + MessageElementFlag::SevenTVEmote, + emote.value()->zeroWidth, + }; + } + } + + // Check for global emotes + + emote = globalFfzEmotes->emote(name); + if (emote) + { + return { + emote, + MessageElementFlag::FfzEmote, + false, + }; + } + + emote = globalBttvEmotes->emote(name); + if (emote) + { + return { + emote, + MessageElementFlag::BttvEmote, + zeroWidthEmotes.contains(name.string), + }; + } + + emote = globalSeventvEmotes->globalEmote(name); + if (emote) + { + return { + emote, + MessageElementFlag::SevenTVEmote, + emote.value()->zeroWidth, + }; + } + + return { + {}, + {}, + false, + }; +} + } // namespace namespace chatterino { @@ -400,36 +487,6 @@ MessagePtr makeSystemMessage(const QString &text, const QTime &time) MessageBuilder::MessageBuilder() : message_(std::make_shared()) - , ircMessage(nullptr) -{ -} - -MessageBuilder::MessageBuilder(Channel *_channel, - const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args) - : twitchChannel(dynamic_cast(_channel)) - , message_(std::make_shared()) - , channel(_channel) - , ircMessage(_ircMessage) - , args(_args) - , tags(this->ircMessage->tags()) - , originalMessage_(_ircMessage->content()) - , action_(_ircMessage->isAction()) -{ -} - -MessageBuilder::MessageBuilder(Channel *_channel, - const Communi::IrcMessage *_ircMessage, - const MessageParseArgs &_args, QString content, - bool isAction) - : twitchChannel(dynamic_cast(_channel)) - , message_(std::make_shared()) - , channel(_channel) - , ircMessage(_ircMessage) - , args(_args) - , tags(this->ircMessage->tags()) - , originalMessage_(content) - , action_(isAction) { } @@ -1013,14 +1070,14 @@ Message &MessageBuilder::message() return *this->message_; } -MessagePtr MessageBuilder::release() +MessagePtrMut MessageBuilder::release() { std::shared_ptr ptr; this->message_.swap(ptr); return ptr; } -std::weak_ptr MessageBuilder::weakOf() +std::weak_ptr MessageBuilder::weakOf() { return this->message_; } @@ -1072,221 +1129,36 @@ void MessageBuilder::addLink(const linkparser::Parsed &parsedLink, getApp()->getLinkResolver()->resolve(el->linkInfo()); } -bool MessageBuilder::isIgnored() const +bool MessageBuilder::isIgnored(const QString &originalMessage, + const QString &userID, const Channel *channel) { return isIgnoredMessage({ - /*.message = */ this->originalMessage_, - /*.twitchUserID = */ this->tags.value("user-id").toString(), - /*.isMod = */ this->channel->isMod(), - /*.isBroadcaster = */ this->channel->isBroadcaster(), + .message = originalMessage, + .twitchUserID = userID, + .isMod = channel->isMod(), + .isBroadcaster = channel->isBroadcaster(), }); } -bool MessageBuilder::isIgnoredReply() const +void MessageBuilder::triggerHighlights(const Channel *channel, + const HighlightAlert &alert) { - return isIgnoredMessage({ - /*.message = */ this->originalMessage_, - /*.twitchUserID = */ - this->tags.value("reply-parent-user-id").toString(), - /*.isMod = */ this->channel->isMod(), - /*.isBroadcaster = */ this->channel->isBroadcaster(), - }); -} - -void MessageBuilder::triggerHighlights() -{ - if (this->historicalMessage_) + if (!alert.windowAlert && !alert.playSound) { - // Do nothing. Highlights should not be triggered on historical messages. return; } - - actuallyTriggerHighlights(this->channel->getName(), this->highlightSound_, - this->highlightSoundCustomUrl_, - this->highlightAlert_); -} - -MessagePtr MessageBuilder::build() -{ - assert(this->ircMessage != nullptr); - assert(this->channel != nullptr); - - // PARSE - this->userId_ = this->ircMessage->tag("user-id").toString(); - - this->parse(); - - if (this->userName == this->channel->getName()) - { - this->senderIsBroadcaster = true; - } - - this->message().channelName = this->channel->getName(); - - this->parseMessageID(); - - this->parseRoomID(); - - // If it is a reward it has to be appended first - if (this->args.channelPointRewardId != "") - { - assert(this->twitchChannel != nullptr); - const auto &reward = this->twitchChannel->channelPointReward( - this->args.channelPointRewardId); - if (reward) - { - this->appendChannelPointRewardMessage( - *reward, this->channel->isMod(), - this->channel->isBroadcaster()); - } - } - - this->appendChannelName(); - - if (this->tags.contains("rm-deleted")) - { - this->message().flags.set(MessageFlag::Disabled); - } - - this->historicalMessage_ = this->tags.contains("historical"); - - if (this->tags.contains("msg-id") && - this->tags["msg-id"].toString().split(';').contains( - "highlighted-message")) - { - this->message().flags.set(MessageFlag::RedeemedHighlight); - } - - if (this->tags.contains("first-msg") && - this->tags["first-msg"].toString() == "1") - { - this->message().flags.set(MessageFlag::FirstMessage); - } - - if (this->tags.contains("pinned-chat-paid-amount")) - { - this->message().flags.set(MessageFlag::ElevatedMessage); - } - - if (this->tags.contains("bits")) - { - this->message().flags.set(MessageFlag::CheerMessage); - } - - // reply threads - this->parseThread(); - - // timestamp - this->message().serverReceivedTime = calculateMessageTime(this->ircMessage); - this->emplace(this->message().serverReceivedTime.time()); - - if (this->shouldAddModerationElements()) - { - this->emplace(); - } - - this->appendTwitchBadges(); - - this->appendChatterinoBadges(); - this->appendFfzBadges(); - this->appendSeventvBadges(); - - this->appendUsername(); - - // QString bits; - auto iterator = this->tags.find("bits"); - if (iterator != this->tags.end()) - { - this->hasBits_ = true; - this->bitsLeft = iterator.value().toInt(); - this->bits = iterator.value().toString(); - } - - // Twitch emotes - auto twitchEmotes = parseTwitchEmotes(this->tags, this->originalMessage_, - this->messageOffset_); - - // This runs through all ignored phrases and runs its replacements on this->originalMessage_ - processIgnorePhrases(*getSettings()->ignoredMessages.readOnly(), - this->originalMessage_, twitchEmotes); - - std::sort(twitchEmotes.begin(), twitchEmotes.end(), - [](const auto &a, const auto &b) { - return a.start < b.start; - }); - twitchEmotes.erase(std::unique(twitchEmotes.begin(), twitchEmotes.end(), - [](const auto &first, const auto &second) { - return first.start == second.start; - }), - twitchEmotes.end()); - - // words - QStringList splits = this->originalMessage_.split(' '); - - this->addWords(splits, twitchEmotes); - - QString stylizedUsername = stylizeUsername(this->userName, this->message()); - - this->message().messageText = this->originalMessage_; - this->message().searchText = - stylizedUsername + " " + this->message().localizedName + " " + - this->userName + ": " + this->originalMessage_ + " " + - this->message().searchText; - - // highlights - this->parseHighlights(); - - // highlighting incoming whispers if requested per setting - if (this->args.isReceivedWhisper && getSettings()->highlightInlineWhispers) - { - this->message().flags.set(MessageFlag::HighlightedWhisper, true); - this->message().highlightColor = - ColorProvider::instance().color(ColorType::Whisper); - } - - if (this->thread_) - { - auto &img = getResources().buttons.replyThreadDark; - this->emplace( - Image::fromResourcePixmap(img, 0.15), 2, Qt::gray, - MessageElementFlag::ReplyButton) - ->setLink({Link::ViewThread, this->thread_->rootId()}); - } - else - { - auto &img = getResources().buttons.replyDark; - this->emplace( - Image::fromResourcePixmap(img, 0.15), 2, Qt::gray, - MessageElementFlag::ReplyButton) - ->setLink({Link::ReplyToMessage, this->message().id}); - } - - return this->release(); -} - -void MessageBuilder::setThread(std::shared_ptr thread) -{ - this->thread_ = std::move(thread); -} - -void MessageBuilder::setParent(MessagePtr parent) -{ - this->parent_ = std::move(parent); -} - -void MessageBuilder::setMessageOffset(int offset) -{ - this->messageOffset_ = offset; + actuallyTriggerHighlights(channel->getName(), alert.playSound, + alert.customSound, alert.windowAlert); } void MessageBuilder::appendChannelPointRewardMessage( const ChannelPointReward &reward, bool isMod, bool isBroadcaster) { if (isIgnoredMessage({ - /*.message = */ "", - /*.twitchUserID = */ reward.user.id, - /*.isMod = */ isMod, - /*.isBroadcaster = */ isBroadcaster, + .message = {}, + .twitchUserID = reward.user.id, + .isMod = isMod, + .isBroadcaster = isBroadcaster, })) { return; @@ -1339,7 +1211,10 @@ void MessageBuilder::appendChannelPointRewardMessage( textList.append({redeemed, reward.title, QString::number(reward.cost)}); this->message().messageText = textList.join(" "); this->message().searchText = textList.join(" "); - this->message().loginName = reward.user.login; + if (!reward.user.login.isEmpty()) + { + this->message().loginName = reward.user.login; + } this->message().reward = std::make_shared(reward); } @@ -1763,9 +1638,10 @@ std::pair MessageBuilder::makeAutomodMessage( {}, {}, action.target.login, action.message, message2->flags); if (highlighted) { - actuallyTriggerHighlights(channelName, highlightResult.playSound, - highlightResult.customSoundUrl, - highlightResult.alert); + actuallyTriggerHighlights( + channelName, highlightResult.playSound, + highlightResult.customSoundUrl.value_or(QUrl{}), + highlightResult.alert); } return std::make_pair(message1, message2); @@ -2022,16 +1898,217 @@ MessagePtr MessageBuilder::makeLowTrustUpdateMessage( return builder.release(); } -void MessageBuilder::addTextOrEmoji(EmotePtr emote) +std::pair MessageBuilder::makeIrcMessage( + /* mutable */ Channel *channel, const Communi::IrcMessage *ircMessage, + const MessageParseArgs &args, /* mutable */ QString content, + const QString::size_type messageOffset, + const std::shared_ptr &thread, const MessagePtr &parent) +{ + assert(ircMessage != nullptr); + assert(channel != nullptr); + + auto tags = ircMessage->tags(); + if (args.allowIgnore) + { + bool ignored = MessageBuilder::isIgnored( + content, tags.value("user-id").toString(), channel); + if (ignored) + { + return {}; + } + } + + auto *twitchChannel = dynamic_cast(channel); + + auto userID = tags.value("user-id").toString(); + + MessageBuilder builder; + builder.parseUsernameColor(tags, userID); + + if (args.isAction) + { + builder.textColor_ = builder.message_->usernameColor; + builder->flags.set(MessageFlag::Action); + } + + builder.parseUsername(ircMessage, twitchChannel, + args.trimSubscriberUsername); + + builder->flags.set(MessageFlag::Collapsed); + + bool senderIsBroadcaster = builder->loginName == channel->getName(); + + builder->channelName = channel->getName(); + + builder.parseMessageID(tags); + + MessageBuilder::parseRoomID(tags, twitchChannel); + twitchChannel = builder.parseSharedChatInfo(tags, twitchChannel); + + // If it is a reward it has to be appended first + if (!args.channelPointRewardId.isEmpty()) + { + assert(twitchChannel != nullptr); + auto reward = + twitchChannel->channelPointReward(args.channelPointRewardId); + if (reward) + { + builder.appendChannelPointRewardMessage(*reward, channel->isMod(), + channel->isBroadcaster()); + } + } + + builder.appendChannelName(channel); + + if (tags.contains("rm-deleted")) + { + builder->flags.set(MessageFlag::Disabled); + } + + if (tags.contains("msg-id") && + tags["msg-id"].toString().split(';').contains("highlighted-message")) + { + builder->flags.set(MessageFlag::RedeemedHighlight); + } + + if (tags.contains("first-msg") && tags["first-msg"].toString() == "1") + { + builder->flags.set(MessageFlag::FirstMessage); + } + + if (tags.contains("pinned-chat-paid-amount")) + { + builder->flags.set(MessageFlag::ElevatedMessage); + } + + if (tags.contains("bits")) + { + builder->flags.set(MessageFlag::CheerMessage); + } + + // reply threads + builder.parseThread(content, tags, channel, thread, parent); + + // timestamp + builder->serverReceivedTime = calculateMessageTime(ircMessage); + builder.emplace(builder->serverReceivedTime.time()); + + bool shouldAddModerationElements = [&] { + if (senderIsBroadcaster) + { + // You cannot timeout the broadcaster + return false; + } + + if (tags.value("user-type").toString() == "mod" && + !args.isStaffOrBroadcaster) + { + // You cannot timeout moderators UNLESS you are Twitch Staff or the broadcaster of the channel + return false; + } + + return true; + }(); + if (shouldAddModerationElements) + { + builder.emplace(); + } + + builder.appendTwitchBadges(tags, twitchChannel); + + builder.appendChatterinoBadges(userID); + builder.appendFfzBadges(twitchChannel, userID); + builder.appendSeventvBadges(userID); + + builder.appendUsername(tags, args); + + TextState textState{.twitchChannel = twitchChannel}; + QString bits; + + auto iterator = tags.find("bits"); + if (iterator != tags.end()) + { + textState.hasBits = true; + textState.bitsLeft = iterator.value().toInt(); + bits = iterator.value().toString(); + } + + // Twitch emotes + auto twitchEmotes = + parseTwitchEmotes(tags, content, static_cast(messageOffset)); + + // This runs through all ignored phrases and runs its replacements on content + processIgnorePhrases(*getSettings()->ignoredMessages.readOnly(), content, + twitchEmotes); + + std::ranges::sort(twitchEmotes, [](const auto &a, const auto &b) { + return a.start < b.start; + }); + auto uniqueEmotes = std::ranges::unique( + twitchEmotes, [](const auto &first, const auto &second) { + return first.start == second.start; + }); + twitchEmotes.erase(uniqueEmotes.begin(), uniqueEmotes.end()); + + // words + QStringList splits = content.split(' '); + + builder.addWords(splits, twitchEmotes, textState); + + QString stylizedUsername = + stylizeUsername(builder->loginName, builder.message()); + + builder->messageText = content; + builder->searchText = stylizedUsername + " " + builder->localizedName + + " " + builder->loginName + ": " + content + " " + + builder->searchText; + + // highlights + HighlightAlert highlight = builder.parseHighlights(tags, content, args); + if (tags.contains("historical")) + { + highlight.playSound = false; + highlight.windowAlert = false; + } + + // highlighting incoming whispers if requested per setting + if (args.isReceivedWhisper && getSettings()->highlightInlineWhispers) + { + builder->flags.set(MessageFlag::HighlightedWhisper); + builder->highlightColor = + ColorProvider::instance().color(ColorType::Whisper); + } + + if (thread) + { + auto &img = getResources().buttons.replyThreadDark; + builder + .emplace(Image::fromResourcePixmap(img, 0.15), + 2, Qt::gray, + MessageElementFlag::ReplyButton) + ->setLink({Link::ViewThread, thread->rootId()}); + } + else + { + auto &img = getResources().buttons.replyDark; + builder + .emplace(Image::fromResourcePixmap(img, 0.15), + 2, Qt::gray, + MessageElementFlag::ReplyButton) + ->setLink({Link::ReplyToMessage, builder->id}); + } + + return {builder.release(), highlight}; +} + +void MessageBuilder::addEmoji(const EmotePtr &emote) { this->emplace(emote, MessageElementFlag::EmojiAll); } -void MessageBuilder::addTextOrEmoji(const QString &string_) +void MessageBuilder::addTextOrEmote(TextState &state, QString string) { - auto string = QString(string_); - - if (this->hasBits_ && this->tryParseCheermote(string)) + if (state.hasBits && this->tryAppendCheermote(state, string)) { // This string was parsed as a cheermote return; @@ -2042,7 +2119,7 @@ void MessageBuilder::addTextOrEmoji(const QString &string_) // Emote name: "forsenPuke" - if string in ignoredEmotes // Will match emote regardless of source (i.e. bttv, ffz) // Emote source + name: "bttv:nyanPls" - if (this->tryAppendEmote({string})) + if (this->tryAppendEmote(state.twitchChannel, {string})) { // Successfully appended an emote return; @@ -2067,10 +2144,10 @@ void MessageBuilder::addTextOrEmoji(const QString &string_) QString username = match.captured(1); auto originalTextColor = textColor; - if (this->twitchChannel != nullptr) + if (state.twitchChannel != nullptr) { if (auto userColor = - this->twitchChannel->getUserColor(username); + state.twitchChannel->getUserColor(username); userColor.isValid()) { textColor = userColor; @@ -2093,17 +2170,17 @@ void MessageBuilder::addTextOrEmoji(const QString &string_) } } - if (this->twitchChannel != nullptr && getSettings()->findAllUsernames) + if (state.twitchChannel != nullptr && getSettings()->findAllUsernames) { auto match = allUsernamesMentionRegex.match(string); QString username = match.captured(1); if (match.hasMatch() && - this->twitchChannel->accessChatters()->contains(username)) + state.twitchChannel->accessChatters()->contains(username)) { auto originalTextColor = textColor; - if (auto userColor = this->twitchChannel->getUserColor(username); + if (auto userColor = state.twitchChannel->getUserColor(username); userColor.isValid()) { textColor = userColor; @@ -2155,37 +2232,24 @@ TextElement *MessageBuilder::emplaceSystemTextAndUpdate(const QString &text, MessageColor::System); } -void MessageBuilder::parse() -{ - this->parseUsernameColor(); - - if (this->action_) - { - this->textColor_ = this->usernameColor_; - this->message().flags.set(MessageFlag::Action); - } - - this->parseUsername(); - - this->message().flags.set(MessageFlag::Collapsed); -} - -void MessageBuilder::parseUsernameColor() +void MessageBuilder::parseUsernameColor(const QVariantMap &tags, + const QString &userID) { const auto *userData = getApp()->getUserData(); assert(userData != nullptr); - if (const auto &user = userData->getUser(this->userId_)) + if (const auto &user = userData->getUser(userID)) { if (user->color) { this->usernameColor_ = user->color.value(); + this->message().usernameColor = this->usernameColor_; return; } } - const auto iterator = this->tags.find("color"); - if (iterator != this->tags.end()) + const auto iterator = tags.find("color"); + if (iterator != tags.end()) { if (const auto color = iterator.value().toString(); !color.isEmpty()) { @@ -2195,117 +2259,140 @@ void MessageBuilder::parseUsernameColor() } } - if (getSettings()->colorizeNicknames && this->tags.contains("user-id")) + if (getSettings()->colorizeNicknames && tags.contains("user-id")) { - this->usernameColor_ = - getRandomColor(this->tags.value("user-id").toString()); + this->usernameColor_ = getRandomColor(tags.value("user-id").toString()); this->message().usernameColor = this->usernameColor_; } } -void MessageBuilder::parseUsername() +void MessageBuilder::parseUsername(const Communi::IrcMessage *ircMessage, + TwitchChannel *twitchChannel, + bool trimSubscriberUsername) { // username - this->userName = this->ircMessage->nick(); + auto userName = ircMessage->nick(); - this->message().loginName = this->userName; - - if (this->userName.isEmpty() || this->args.trimSubscriberUsername) + if (userName.isEmpty() || trimSubscriberUsername) { - this->userName = this->tags.value(QLatin1String("login")).toString(); + userName = ircMessage->tag("login").toString(); } - // display name - // auto displayNameVariant = this->tags.value("display-name"); - // if (displayNameVariant.isValid()) { - // this->userName = displayNameVariant.toString() + " (" + - // this->userName + ")"; - // } - - this->message().loginName = this->userName; - if (this->twitchChannel != nullptr) + this->message_->loginName = userName; + if (twitchChannel != nullptr) { - this->twitchChannel->setUserColor(this->userName, this->usernameColor_); + twitchChannel->setUserColor(userName, this->message_->usernameColor); } // Update current user color if this is our message auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); - if (this->ircMessage->nick() == currentUser->getUserName()) + if (ircMessage->nick() == currentUser->getUserName()) { - currentUser->setColor(this->usernameColor_); + currentUser->setColor(this->message_->usernameColor); } } -void MessageBuilder::parseMessageID() +void MessageBuilder::parseMessageID(const QVariantMap &tags) { - auto iterator = this->tags.find("id"); + auto iterator = tags.find("id"); - if (iterator != this->tags.end()) + if (iterator != tags.end()) { this->message().id = iterator.value().toString(); } } -void MessageBuilder::parseRoomID() +QString MessageBuilder::parseRoomID(const QVariantMap &tags, + TwitchChannel *twitchChannel) { - if (this->twitchChannel == nullptr) + if (twitchChannel == nullptr) { - return; + return {}; } - auto iterator = this->tags.find("room-id"); + auto iterator = tags.find("room-id"); - if (iterator != std::end(this->tags)) + if (iterator != std::end(tags)) { - this->roomID_ = iterator.value().toString(); - - if (this->twitchChannel->roomId().isEmpty()) + auto roomID = iterator->toString(); + if (twitchChannel->roomId() != roomID) { - this->twitchChannel->setRoomId(this->roomID_); - } - - if (auto it = this->tags.find("source-room-id"); it != this->tags.end()) - { - auto sourceRoom = it.value().toString(); - if (this->roomID_ != sourceRoom) + if (twitchChannel->roomId().isEmpty()) { - this->message().flags.set(MessageFlag::SharedMessage); + twitchChannel->setRoomId(roomID); + } + else + { + qCWarning(chatterinoTwitch) + << "The room-ID of the received message doesn't match the " + "room-ID of the channel - received:" + << roomID << "channel:" << twitchChannel->roomId(); + } + } + return roomID; + } - auto sourceChan = - getApp()->getTwitch()->getChannelOrEmptyByID(sourceRoom); - if (sourceChan && !sourceChan->isEmpty()) + return {}; +} + +TwitchChannel *MessageBuilder::parseSharedChatInfo(const QVariantMap &tags, + TwitchChannel *twitchChannel) +{ + if (!twitchChannel) + { + return twitchChannel; + } + + if (auto it = tags.find("source-room-id"); it != tags.end()) + { + auto sourceRoom = it.value().toString(); + if (twitchChannel->roomId() != sourceRoom) + { + this->message().flags.set(MessageFlag::SharedMessage); + + auto sourceChan = + getApp()->getTwitch()->getChannelOrEmptyByID(sourceRoom); + if (sourceChan && !sourceChan->isEmpty()) + { + // avoid duplicate pings + this->message().flags.set( + MessageFlag::DoNotTriggerNotification); + + auto *chan = dynamic_cast(sourceChan.get()); + if (chan) { - this->sourceChannel = - dynamic_cast(sourceChan.get()); - // avoid duplicate pings - this->message().flags.set( - MessageFlag::DoNotTriggerNotification); + return chan; } } } } + return twitchChannel; } -void MessageBuilder::parseThread() +void MessageBuilder::parseThread(const QString &messageContent, + const QVariantMap &tags, + const Channel *channel, + const std::shared_ptr &thread, + const MessagePtr &parent) { - if (this->thread_) + if (thread) { // set references - this->message().replyThread = this->thread_; - this->message().replyParent = this->parent_; - this->thread_->addToThread(this->weakOf()); + this->message().replyThread = thread; + this->message().replyParent = parent; + thread->addToThread(std::weak_ptr{this->message_}); // enable reply flag this->message().flags.set(MessageFlag::ReplyMessage); MessagePtr threadRoot; - if (!this->parent_) + if (!parent) { - threadRoot = this->thread_->root(); + threadRoot = thread->root(); } else { - threadRoot = this->parent_; + threadRoot = parent; } QString usernameText = @@ -2317,7 +2404,7 @@ void MessageBuilder::parseThread() this->emplace( "Replying to", MessageElementFlag::RepliedMessage, MessageColor::System, FontStyle::ChatMediumSmall) - ->setLink({Link::ViewThread, this->thread_->rootId()}); + ->setLink({Link::ViewThread, thread->rootId()}); this->emplace( "@" + usernameText + @@ -2336,18 +2423,17 @@ void MessageBuilder::parseThread() MessageElementFlags({MessageElementFlag::RepliedMessage, MessageElementFlag::Text}), color, FontStyle::ChatMediumSmall) - ->setLink({Link::ViewThread, this->thread_->rootId()}); + ->setLink({Link::ViewThread, thread->rootId()}); } - else if (this->tags.find("reply-parent-msg-id") != this->tags.end()) + else if (tags.find("reply-parent-msg-id") != tags.end()) { // Message is a reply but we couldn't find the original message. // Render the message using the additional reply tags - auto replyDisplayName = this->tags.find("reply-parent-display-name"); - auto replyBody = this->tags.find("reply-parent-msg-body"); + auto replyDisplayName = tags.find("reply-parent-display-name"); + auto replyBody = tags.find("reply-parent-msg-body"); - if (replyDisplayName != this->tags.end() && - replyBody != this->tags.end()) + if (replyDisplayName != tags.end() && replyBody != tags.end()) { QString body; @@ -2356,7 +2442,10 @@ void MessageBuilder::parseThread() "Replying to", MessageElementFlag::RepliedMessage, MessageColor::System, FontStyle::ChatMediumSmall); - if (this->isIgnoredReply()) + bool ignored = MessageBuilder::isIgnored( + messageContent, tags.value("reply-parent-user-id").toString(), + channel); + if (ignored) { body = QString("[Blocked user]"); } @@ -2380,67 +2469,76 @@ void MessageBuilder::parseThread() } } -void MessageBuilder::parseHighlights() +HighlightAlert MessageBuilder::parseHighlights(const QVariantMap &tags, + const QString &originalMessage, + const MessageParseArgs &args) { if (getSettings()->isBlacklistedUser(this->message().loginName)) { // Do nothing. We ignore highlights from this user. - return; + return {}; } - auto badges = parseBadgeTag(this->tags); + auto badges = parseBadgeTag(tags); auto [highlighted, highlightResult] = getApp()->getHighlights()->check( - this->args, badges, this->message().loginName, this->originalMessage_, + args, badges, this->message().loginName, originalMessage, this->message().flags); if (!highlighted) { - return; + return {}; } // This message triggered one or more highlights, act upon the highlight result this->message().flags.set(MessageFlag::Highlighted); - this->highlightAlert_ = highlightResult.alert; - - this->highlightSound_ = highlightResult.playSound; - this->highlightSoundCustomUrl_ = highlightResult.customSoundUrl; - this->message().highlightColor = highlightResult.color; if (highlightResult.showInMentions) { this->message().flags.set(MessageFlag::ShowInMentions); } + + auto customSound = [&] { + if (highlightResult.customSoundUrl) + { + return *highlightResult.customSoundUrl; + } + return QUrl{}; + }(); + return { + .customSound = customSound, + .playSound = highlightResult.playSound, + .windowAlert = highlightResult.alert, + }; } -void MessageBuilder::appendChannelName() +void MessageBuilder::appendChannelName(const Channel *channel) { - QString channelName("#" + this->channel->getName()); - Link link(Link::JumpToChannel, this->channel->getName()); + QString channelName("#" + channel->getName()); + Link link(Link::JumpToChannel, channel->getName()); this->emplace(channelName, MessageElementFlag::ChannelName, MessageColor::System) ->setLink(link); } -void MessageBuilder::appendUsername() +void MessageBuilder::appendUsername(const QVariantMap &tags, + const MessageParseArgs &args) { auto *app = getApp(); - QString username = this->userName; - this->message().loginName = username; + QString username = this->message_->loginName; QString localizedName; - auto iterator = this->tags.find("display-name"); - if (iterator != this->tags.end()) + auto iterator = tags.find("display-name"); + if (iterator != tags.end()) { QString displayName = parseTagString(iterator.value().toString()).trimmed(); - if (QString::compare(displayName, this->userName, - Qt::CaseInsensitive) == 0) + if (QString::compare(displayName, username, Qt::CaseInsensitive) == 0) { username = displayName; @@ -2457,13 +2555,13 @@ void MessageBuilder::appendUsername() QString usernameText = stylizeUsername(username, this->message()); - if (this->args.isSentWhisper) + if (args.isSentWhisper) { // TODO(pajlada): Re-implement // userDisplayString += // IrcManager::instance().getUser().getUserName(); } - else if (this->args.isReceivedWhisper) + else if (args.isReceivedWhisper) { // Sender username this->emplace(usernameText, MessageElementFlag::Username, @@ -2488,7 +2586,7 @@ void MessageBuilder::appendUsername() } else { - if (!this->action_) + if (!args.isAction) { usernameText += ":"; } @@ -2500,157 +2598,53 @@ void MessageBuilder::appendUsername() } } -const TwitchChannel *MessageBuilder::getSourceChannel() const +Outcome MessageBuilder::tryAppendEmote(TwitchChannel *twitchChannel, + const EmoteName &name) { - if (this->sourceChannel != nullptr) + auto [emote, flags, zeroWidth] = parseEmote(twitchChannel, name); + + if (!emote) { - return this->sourceChannel; + return Failure; } - return this->twitchChannel; -} - -std::tuple, MessageElementFlags, bool> - MessageBuilder::parseEmote(const EmoteName &name) const -{ - // Emote order: - // - FrankerFaceZ Channel - // - BetterTTV Channel - // - 7TV Channel - // - FrankerFaceZ Global - // - BetterTTV Global - // - 7TV Global - - const auto *globalFfzEmotes = getApp()->getFfzEmotes(); - const auto *globalBttvEmotes = getApp()->getBttvEmotes(); - const auto *globalSeventvEmotes = getApp()->getSeventvEmotes(); - - const auto *sourceChannel = this->getSourceChannel(); - - std::optional emote{}; - - if (sourceChannel != nullptr) + if (zeroWidth && getSettings()->enableZeroWidthEmotes && !this->isEmpty()) { - // Check for channel emotes - - emote = sourceChannel->ffzEmote(name); - if (emote) + // Attempt to merge current zero-width emote into any previous emotes + auto *asEmote = dynamic_cast(&this->back()); + if (asEmote) { - return { - emote, - MessageElementFlag::FfzEmote, - false, - }; + // Make sure to access asEmote before taking ownership when releasing + auto baseEmote = asEmote->getEmote(); + // Need to remove EmoteElement and replace with LayeredEmoteElement + auto baseEmoteElement = this->releaseBack(); + + std::vector layers = { + {baseEmote, baseEmoteElement->getFlags()}, {*emote, flags}}; + this->emplace( + std::move(layers), baseEmoteElement->getFlags() | flags, + this->textColor_); + return Success; } - emote = sourceChannel->bttvEmote(name); - if (emote) + auto *asLayered = dynamic_cast(&this->back()); + if (asLayered) { - return { - emote, - MessageElementFlag::BttvEmote, - false, - }; + asLayered->addEmoteLayer({*emote, flags}); + asLayered->addFlags(flags); + return Success; } - emote = sourceChannel->seventvEmote(name); - if (emote) - { - return { - emote, - MessageElementFlag::SevenTVEmote, - emote.value()->zeroWidth, - }; - } + // No emote to merge with, just show as regular emote } - // Check for global emotes - - emote = globalFfzEmotes->emote(name); - if (emote) - { - return { - emote, - MessageElementFlag::FfzEmote, - false, - }; - } - - emote = globalBttvEmotes->emote(name); - if (emote) - { - return { - emote, - MessageElementFlag::BttvEmote, - zeroWidthEmotes.contains(name.string), - }; - } - - emote = globalSeventvEmotes->globalEmote(name); - if (emote) - { - return { - emote, - MessageElementFlag::SevenTVEmote, - emote.value()->zeroWidth, - }; - } - - return { - {}, - {}, - false, - }; -} - -Outcome MessageBuilder::tryAppendEmote(const EmoteName &name) -{ - const auto [emote, flags, zeroWidth] = this->parseEmote(name); - - if (emote) - { - if (zeroWidth && getSettings()->enableZeroWidthEmotes && - !this->isEmpty()) - { - // Attempt to merge current zero-width emote into any previous emotes - auto *asEmote = dynamic_cast(&this->back()); - if (asEmote) - { - // Make sure to access asEmote before taking ownership when releasing - auto baseEmote = asEmote->getEmote(); - // Need to remove EmoteElement and replace with LayeredEmoteElement - auto baseEmoteElement = this->releaseBack(); - - std::vector layers = { - {baseEmote, baseEmoteElement->getFlags()}, {*emote, flags}}; - this->emplace( - std::move(layers), baseEmoteElement->getFlags() | flags, - this->textColor_); - return Success; - } - - auto *asLayered = - dynamic_cast(&this->back()); - if (asLayered) - { - asLayered->addEmoteLayer({*emote, flags}); - asLayered->addFlags(flags); - return Success; - } - - // No emote to merge with, just show as regular emote - } - - this->emplace(*emote, flags, this->textColor_); - return Success; - } - - return Failure; + this->emplace(*emote, flags, this->textColor_); + return Success; } void MessageBuilder::addWords( const QStringList &words, - const std::vector &twitchEmotes) + const std::vector &twitchEmotes, TextState &state) { // cursor currently indicates what character index we're currently operating in the full list of words int cursor = 0; @@ -2700,14 +2694,19 @@ void MessageBuilder::addWords( // 1. Add text before the emote QString preText = word.left(currentTwitchEmote.start - cursor); - for (auto &variant : + for (auto variant : getApp()->getEmotes()->getEmojis()->parse(preText)) { - boost::apply_visitor( - [&](auto &&arg) { - this->addTextOrEmoji(arg); - }, - variant); + boost::apply_visitor(variant::Overloaded{ + [&](const EmotePtr &emote) { + this->addEmoji(emote); + }, + [&](QString text) { + this->addTextOrEmote( + state, std::move(text)); + }, + }, + variant); } cursor += preText.size(); @@ -2721,79 +2720,84 @@ void MessageBuilder::addWords( } // split words - for (auto &variant : getApp()->getEmotes()->getEmojis()->parse(word)) + for (auto variant : getApp()->getEmotes()->getEmojis()->parse(word)) { - boost::apply_visitor( - [&](auto &&arg) { - this->addTextOrEmoji(arg); - }, - variant); + boost::apply_visitor(variant::Overloaded{ + [&](const EmotePtr &emote) { + this->addEmoji(emote); + }, + [&](QString text) { + this->addTextOrEmote(state, + std::move(text)); + }, + }, + variant); } cursor += word.size() + 1; } } -void MessageBuilder::appendTwitchBadges() +void MessageBuilder::appendTwitchBadges(const QVariantMap &tags, + TwitchChannel *twitchChannel) { - if (this->twitchChannel == nullptr) + if (twitchChannel == nullptr) { return; } - auto badgeInfos = parseBadgeInfoTag(this->tags); - auto badges = parseBadgeTag(this->tags); - appendBadges(this, badges, badgeInfos, this->twitchChannel); + auto badgeInfos = parseBadgeInfoTag(tags); + auto badges = parseBadgeTag(tags); + appendBadges(this, badges, badgeInfos, twitchChannel); } -void MessageBuilder::appendChatterinoBadges() +void MessageBuilder::appendChatterinoBadges(const QString &userID) { - if (auto badge = getApp()->getChatterinoBadges()->getBadge({this->userId_})) + if (auto badge = getApp()->getChatterinoBadges()->getBadge({userID})) { this->emplace(*badge, MessageElementFlag::BadgeChatterino); } } -void MessageBuilder::appendFfzBadges() +void MessageBuilder::appendFfzBadges(TwitchChannel *twitchChannel, + const QString &userID) { - for (const auto &badge : - getApp()->getFfzBadges()->getUserBadges({this->userId_})) + for (const auto &badge : getApp()->getFfzBadges()->getUserBadges({userID})) { this->emplace( badge.emote, MessageElementFlag::BadgeFfz, badge.color); } - if (this->twitchChannel == nullptr) + if (twitchChannel == nullptr) { return; } - for (const auto &badge : - this->twitchChannel->ffzChannelBadges(this->userId_)) + for (const auto &badge : twitchChannel->ffzChannelBadges(userID)) { this->emplace( badge.emote, MessageElementFlag::BadgeFfz, badge.color); } } -void MessageBuilder::appendSeventvBadges() +void MessageBuilder::appendSeventvBadges(const QString &userID) { - if (auto badge = getApp()->getSeventvBadges()->getBadge({this->userId_})) + if (auto badge = getApp()->getSeventvBadges()->getBadge({userID})) { this->emplace(*badge, MessageElementFlag::BadgeSevenTV); } } -Outcome MessageBuilder::tryParseCheermote(const QString &string) +Outcome MessageBuilder::tryAppendCheermote(TextState &state, + const QString &string) { - if (this->bitsLeft == 0) + if (state.bitsLeft == 0) { return Failure; } - const auto *chan = this->getSourceChannel(); - auto cheerOpt = chan->cheerEmote(string); + auto cheerOpt = state.twitchChannel->cheerEmote(string); if (!cheerOpt) { @@ -2812,7 +2816,7 @@ Outcome MessageBuilder::tryParseCheermote(const QString &string) if (getSettings()->stackBits) { - if (this->bitsStacked) + if (state.bitsStacked) { return Success; } @@ -2830,25 +2834,25 @@ Outcome MessageBuilder::tryParseCheermote(const QString &string) } if (cheerEmote.color != QColor()) { - this->emplace(QString::number(this->bitsLeft), + this->emplace(QString::number(state.bitsLeft), MessageElementFlag::BitsAmount, cheerEmote.color); } - this->bitsStacked = true; + state.bitsStacked = true; return Success; } - if (this->bitsLeft >= cheerValue) + if (state.bitsLeft >= cheerValue) { - this->bitsLeft -= cheerValue; + state.bitsLeft -= cheerValue; } else { QString newString = string; newString.chop(QString::number(cheerValue).length()); - newString += QString::number(cheerValue - this->bitsLeft); + newString += QString::number(cheerValue - state.bitsLeft); - return tryParseCheermote(newString); + return this->tryAppendCheermote(state, newString); } if (cheerEmote.staticEmote) @@ -2873,22 +2877,4 @@ Outcome MessageBuilder::tryParseCheermote(const QString &string) return Success; } -bool MessageBuilder::shouldAddModerationElements() const -{ - if (this->senderIsBroadcaster) - { - // You cannot timeout the broadcaster - return false; - } - - if (this->tags.value("user-type").toString() == "mod" && - !this->args.isStaffOrBroadcaster) - { - // You cannot timeout moderators UNLESS you are Twitch Staff or the broadcaster of the channel - return false; - } - - return true; -} - } // namespace chatterino diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index b353a8bde..45b65095d 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -14,8 +14,6 @@ #include #include -#include -#include #include #include @@ -31,6 +29,7 @@ struct AutomodUserAction; struct AutomodInfoAction; struct Message; using MessagePtr = std::shared_ptr; +using MessagePtrMut = std::shared_ptr; class MessageElement; class TextElement; @@ -68,6 +67,7 @@ struct LiveUpdatesUpdateEmoteSetMessageTag { struct ImageUploaderResultTag { }; +// NOLINTBEGIN(readability-identifier-naming) const SystemMessageTag systemMessage{}; const RaidEntryMessageTag raidEntryMessage{}; const TimeoutMessageTag timeoutMessage{}; @@ -79,6 +79,7 @@ const LiveUpdatesUpdateEmoteSetMessageTag liveUpdatesUpdateEmoteSetMessage{}; // This signifies that you want to construct a message containing the result of // a successful image upload. const ImageUploaderResultTag imageUploaderResultMessage{}; +// NOLINTEND(readability-identifier-naming) MessagePtr makeSystemMessage(const QString &text); MessagePtr makeSystemMessage(const QString &text, const QTime &time); @@ -90,26 +91,22 @@ struct MessageParseArgs { bool trimSubscriberUsername = false; bool isStaffOrBroadcaster = false; bool isSubscriptionMessage = false; + bool allowIgnore = true; + bool isAction = false; QString channelPointRewardId = ""; }; +struct HighlightAlert { + QUrl customSound; + bool playSound = false; + bool windowAlert = false; +}; class MessageBuilder { public: /// Build a message without a base IRC message. MessageBuilder(); - /// Build a message based on an incoming IRC PRIVMSG - explicit MessageBuilder(Channel *_channel, - const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args); - - /// Build a message based on an incoming IRC message (e.g. notice) - explicit MessageBuilder(Channel *_channel, - const Communi::IrcMessage *_ircMessage, - const MessageParseArgs &_args, QString content, - bool isAction); - MessageBuilder(SystemMessageTag, const QString &text, const QTime &time = QTime::currentTime()); MessageBuilder(RaidEntryMessageTag, const QString &text, @@ -157,17 +154,10 @@ public: ~MessageBuilder() = default; - QString userName; - - /// The Twitch Channel the message was received in - TwitchChannel *twitchChannel = nullptr; - /// The Twitch Channel the message was sent in, according to the Shared Chat feature - TwitchChannel *sourceChannel = nullptr; - Message *operator->(); Message &message(); - MessagePtr release(); - std::weak_ptr weakOf(); + MessagePtrMut release(); + std::weak_ptr weakOf(); void append(std::unique_ptr element); void addLink(const linkparser::Parsed &parsedLink, const QString &source); @@ -184,14 +174,8 @@ public: return pointer; } - [[nodiscard]] bool isIgnored() const; - bool isIgnoredReply() const; - void triggerHighlights(); - MessagePtr build(); - - void setThread(std::shared_ptr thread); - void setParent(MessagePtr parent); - void setMessageOffset(int offset); + static void triggerHighlights(const Channel *channel, + const HighlightAlert &alert); void appendChannelPointRewardMessage(const ChannelPointReward &reward, bool isMod, bool isBroadcaster); @@ -231,96 +215,124 @@ public: static MessagePtr makeLowTrustUpdateMessage( const PubSubLowTrustUsersMessage &action); -protected: - void addTextOrEmoji(EmotePtr emote); - void addTextOrEmoji(const QString &string_); + /// @brief Builds a message out of an `ircMessage`. + /// + /// Building a message won't cause highlights to be triggered. They will + /// only be parsed. To trigger highlights (play sound etc.), use + /// triggerHighlights(). + /// + /// @param channel The channel this message was sent to. Must not be + /// `nullptr`. + /// @param ircMessage The original message. This can be any message + /// (PRIVMSG, USERNOTICE, etc.). Its content is not + /// accessed through this parameter but through `content`, + /// as the content might be inside a tag (e.g. gifts in a + /// USERNOTICE). + /// @param args Arguments from parsing a chat message. + /// @param content The message text. This isn't always the entire text. In + /// replies, the leading mention can be cut off. + /// See `messageOffset`. + /// @param messageOffset Starting offset to be used on index-based + /// operations on `content` such as parsing emotes. + /// For example: + /// ircMessage = "@hi there" + /// content = "there" + /// messageOffset_ = 4 + /// The index 6 would resolve to 6 - 4 = 2 => 'e' + /// @param thread The reply thread this message is part of. If there's no + /// thread, this is an empty `shared_ptr`. + /// @param parent The direct parent this message is replying to. This does + /// not need to be the `thread`s root. If this message isn't + /// replying to anything, this is an empty `shared_ptr`. + /// + /// @returns The built message and a highlight result. If the message is + /// ignored (e.g. from a blocked user), then the returned pointer + /// will be en empty `shared_ptr`. + static std::pair makeIrcMessage( + Channel *channel, const Communi::IrcMessage *ircMessage, + const MessageParseArgs &args, QString content, + QString::size_type messageOffset, + const std::shared_ptr &thread = {}, + const MessagePtr &parent = {}); + +private: + struct TextState { + TwitchChannel *twitchChannel = nullptr; + bool hasBits = false; + bool bitsStacked = false; + int bitsLeft = 0; + }; + void addEmoji(const EmotePtr &emote); + void addTextOrEmote(TextState &state, QString string); + + Outcome tryAppendCheermote(TextState &state, const QString &string); + Outcome tryAppendEmote(TwitchChannel *twitchChannel, const EmoteName &name); bool isEmpty() const; MessageElement &back(); std::unique_ptr releaseBack(); - MessageColor textColor_ = MessageColor::Text; - // Helper method that emplaces some text stylized as system text // and then appends that text to the QString parameter "toUpdate". // Returns the TextElement that was emplaced. TextElement *emplaceSystemTextAndUpdate(const QString &text, QString &toUpdate); - std::shared_ptr message_; - void parse(); - void parseUsernameColor(); - void parseUsername(); - void parseMessageID(); - void parseRoomID(); + void parseUsernameColor(const QVariantMap &tags, const QString &userID); + void parseUsername(const Communi::IrcMessage *ircMessage, + TwitchChannel *twitchChannel, + bool trimSubscriberUsername); + void parseMessageID(const QVariantMap &tags); + + /// Parses the room-ID this message was received in + /// + /// @returns The room-ID + static QString parseRoomID(const QVariantMap &tags, + TwitchChannel *twitchChannel); + + /// Parses the shared-chat information from this message. + /// + /// @param tags The tags of the received message + /// @param twitchChannel The channel this message was received in + /// @returns The source channel - the channel this message originated from. + /// If there's no channel currently open, @a twitchChannel is + /// returned. + TwitchChannel *parseSharedChatInfo(const QVariantMap &tags, + TwitchChannel *twitchChannel); + // Parse & build thread information into the message // Will read information from thread_ or from IRC tags - void parseThread(); + void parseThread(const QString &messageContent, const QVariantMap &tags, + const Channel *channel, + const std::shared_ptr &thread, + const MessagePtr &parent); // parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function - void parseHighlights(); - void appendChannelName(); - void appendUsername(); + HighlightAlert parseHighlights(const QVariantMap &tags, + const QString &originalMessage, + const MessageParseArgs &args); - /// Return the Twitch Channel this message originated from - /// - /// Useful to handle messages from the "Shared Chat" feature - /// - /// Can return nullptr - const TwitchChannel *getSourceChannel() const; - - std::tuple, MessageElementFlags, bool> parseEmote( - const EmoteName &name) const; - Outcome tryAppendEmote(const EmoteName &name); + void appendChannelName(const Channel *channel); + void appendUsername(const QVariantMap &tags, const MessageParseArgs &args); void addWords(const QStringList &words, - const std::vector &twitchEmotes); + const std::vector &twitchEmotes, + TextState &state); - void appendTwitchBadges(); - void appendChatterinoBadges(); - void appendFfzBadges(); - void appendSeventvBadges(); - Outcome tryParseCheermote(const QString &string); + void appendTwitchBadges(const QVariantMap &tags, + TwitchChannel *twitchChannel); + void appendChatterinoBadges(const QString &userID); + void appendFfzBadges(TwitchChannel *twitchChannel, const QString &userID); + void appendSeventvBadges(const QString &userID); - bool shouldAddModerationElements() const; + [[nodiscard]] static bool isIgnored(const QString &originalMessage, + const QString &userID, + const Channel *channel); - QString roomID_; - bool hasBits_ = false; - QString bits; - int bitsLeft{}; - bool bitsStacked = false; - bool historicalMessage_ = false; - std::shared_ptr thread_; - MessagePtr parent_; - - /** - * Starting offset to be used on index-based operations on `originalMessage_`. - * - * For example: - * originalMessage_ = "there" - * messageOffset_ = 4 - * (the irc message is "hey there") - * - * then the index 6 would resolve to 6 - 4 = 2 => 'e' - */ - int messageOffset_ = 0; - - QString userId_; - bool senderIsBroadcaster{}; - - Channel *channel = nullptr; - const Communi::IrcMessage *ircMessage; - MessageParseArgs args; - const QVariantMap tags; - QString originalMessage_; - - const bool action_{}; + std::shared_ptr message_; + MessageColor textColor_ = MessageColor::Text; QColor usernameColor_ = {153, 153, 153}; - - bool highlightAlert_ = false; - bool highlightSound_ = false; - std::optional highlightSoundCustomUrl_{}; }; } // namespace chatterino diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 7d5bb34a4..fb9d2fa13 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -508,15 +508,20 @@ std::vector parseUserNoticeMessage(Channel *channel, { MessageParseArgs args; args.trimSubscriberUsername = true; + args.allowIgnore = false; - MessageBuilder builder(channel, message, args, content, false); - builder->flags.set(MessageFlag::Subscription); - builder->flags.unset(MessageFlag::Highlighted); - if (mirrored) + auto [built, highlight] = MessageBuilder::makeIrcMessage( + channel, message, args, content, 0); + if (built) { - builder->flags.set(MessageFlag::SharedMessage); + built->flags.set(MessageFlag::Subscription); + built->flags.unset(MessageFlag::Highlighted); + if (mirrored) + { + built->flags.set(MessageFlag::SharedMessage); + } + builtMessages.emplace_back(std::move(built)); } - builtMessages.emplace_back(builder.build()); } } @@ -661,12 +666,13 @@ std::vector parsePrivMessage(Channel *channel, std::vector builtMessages; MessageParseArgs args; - MessageBuilder builder(channel, message, args, message->content(), - message->isAction()); - if (!builder.isIgnored()) + args.isAction = message->isAction(); + auto [built, alert] = MessageBuilder::makeIrcMessage(channel, message, args, + message->content(), 0); + if (built) { - builtMessages.emplace_back(builder.build()); - builder.triggerHighlights(); + builtMessages.emplace_back(std::move(built)); + MessageBuilder::triggerHighlights(channel, alert); } return builtMessages; @@ -709,22 +715,21 @@ std::vector IrcMessageHandler::parseMessageWithReply( { args.channelPointRewardId = it.value().toString(); } - MessageBuilder builder(channel, message, args, content, - privMsg->isAction()); - builder.setMessageOffset(messageOffset); + args.isAction = privMsg->isAction(); auto replyCtx = getReplyContext(tc, message, otherLoaded); - builder.setThread(std::move(replyCtx.thread)); - builder.setParent(std::move(replyCtx.parent)); - if (replyCtx.highlight) - { - builder.message().flags.set(MessageFlag::SubscribedThread); - } + auto [built, alert] = MessageBuilder::makeIrcMessage( + channel, message, args, content, messageOffset, replyCtx.thread, + replyCtx.parent); - if (!builder.isIgnored()) + if (built) { - builtMessages.emplace_back(builder.build()); - builder.triggerHighlights(); + if (replyCtx.highlight) + { + built->flags.set(MessageFlag::SubscribedThread); + } + builtMessages.emplace_back(built); + MessageBuilder::triggerHighlights(channel, alert); } if (message->tags().contains(u"pinned-chat-paid-amount"_s)) @@ -1016,20 +1021,18 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *ircMessage) auto *c = getApp()->getTwitch()->getWhispersChannel().get(); - MessageBuilder builder(c, ircMessage, args, - unescapeZeroWidthJoiner(ircMessage->parameter(1)), - false); - - if (builder.isIgnored()) + auto [message, alert] = MessageBuilder::makeIrcMessage( + c, ircMessage, args, unescapeZeroWidthJoiner(ircMessage->parameter(1)), + 0); + if (!message) { return; } - builder->flags.set(MessageFlag::Whisper); - MessagePtr message = builder.build(); - builder.triggerHighlights(); + message->flags.set(MessageFlag::Whisper); + MessageBuilder::triggerHighlights(c, alert); - getApp()->getTwitch()->setLastUserThatWhisperedMe(builder.userName); + getApp()->getTwitch()->setLastUserThatWhisperedMe(message->loginName); if (message->flags.has(MessageFlag::ShowInMentions)) { @@ -1504,6 +1507,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, { args.isStaffOrBroadcaster = true; } + args.isAction = isAction; auto *channel = dynamic_cast(chan.get()); @@ -1605,24 +1609,22 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, } } - MessageBuilder builder(channel, message, args, content, isAction); - builder.setMessageOffset(messageOffset); + args.allowIgnore = !isSub; + auto [msg, alert] = MessageBuilder::makeIrcMessage( + channel, message, args, content, messageOffset, replyCtx.thread, + replyCtx.parent); - builder.setThread(std::move(replyCtx.thread)); - builder.setParent(std::move(replyCtx.parent)); - if (replyCtx.highlight) - { - builder.message().flags.set(MessageFlag::SubscribedThread); - } - - if (isSub || !builder.isIgnored()) + if (msg) { if (isSub) { - builder->flags.set(MessageFlag::Subscription); - builder->flags.unset(MessageFlag::Highlighted); + msg->flags.set(MessageFlag::Subscription); + msg->flags.unset(MessageFlag::Highlighted); + } + if (replyCtx.highlight) + { + msg->flags.set(MessageFlag::SubscribedThread); } - auto msg = builder.build(); IrcMessageHandler::setSimilarityFlags(msg, chan); @@ -1630,7 +1632,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, (!getSettings()->hideSimilar && getSettings()->shownSimilarTriggerHighlights)) { - builder.triggerHighlights(); + MessageBuilder::triggerHighlights(channel, alert); } const auto highlighted = msg->flags.has(MessageFlag::Highlighted); diff --git a/tests/snapshots/IrcMessageHandler/bad-emotes.json b/tests/snapshots/IrcMessageHandler/bad-emotes.json index 33314c1e8..86714b028 100644 --- a/tests/snapshots/IrcMessageHandler/bad-emotes.json +++ b/tests/snapshots/IrcMessageHandler/bad-emotes.json @@ -153,7 +153,7 @@ "searchText": "mm2pl mm2pl: Kappa ", "serverReceivedTime": "2022-09-03T10:31:42Z", "timeoutUser": "", - "usernameColor": "#ff000000" + "usernameColor": "#ffdaa521" } ] } diff --git a/tests/snapshots/IrcMessageHandler/bad-emotes2.json b/tests/snapshots/IrcMessageHandler/bad-emotes2.json index 4455bf818..fa8cb19e5 100644 --- a/tests/snapshots/IrcMessageHandler/bad-emotes2.json +++ b/tests/snapshots/IrcMessageHandler/bad-emotes2.json @@ -153,7 +153,7 @@ "searchText": "mm2pl mm2pl: Kappa ", "serverReceivedTime": "2022-09-03T10:31:42Z", "timeoutUser": "", - "usernameColor": "#ff000000" + "usernameColor": "#ffdaa521" } ] } diff --git a/tests/snapshots/IrcMessageHandler/emote.json b/tests/snapshots/IrcMessageHandler/emote.json index 5a9ec3fd2..cb8965388 100644 --- a/tests/snapshots/IrcMessageHandler/emote.json +++ b/tests/snapshots/IrcMessageHandler/emote.json @@ -153,7 +153,7 @@ "searchText": "mm2pl mm2pl: Keepo ", "serverReceivedTime": "2022-09-03T10:31:35Z", "timeoutUser": "", - "usernameColor": "#ff000000" + "usernameColor": "#ffdaa521" } ] } diff --git a/tests/snapshots/IrcMessageHandler/emotes.json b/tests/snapshots/IrcMessageHandler/emotes.json index 19d6bd7bf..e7e6d4a02 100644 --- a/tests/snapshots/IrcMessageHandler/emotes.json +++ b/tests/snapshots/IrcMessageHandler/emotes.json @@ -221,7 +221,7 @@ "searchText": "mm2pl mm2pl: Kappa Keepo PogChamp ", "serverReceivedTime": "2022-09-03T10:31:42Z", "timeoutUser": "", - "usernameColor": "#ff000000" + "usernameColor": "#ffdaa521" } ] } diff --git a/tests/src/Filters.cpp b/tests/src/Filters.cpp index b0c728059..67cd48d23 100644 --- a/tests/src/Filters.cpp +++ b/tests/src/Filters.cpp @@ -285,9 +285,9 @@ TEST_F(FiltersF, TypingContextChecks) QString originalMessage = privmsg->content(); - MessageBuilder builder(&channel, privmsg, MessageParseArgs{}); + auto [msg, alert] = MessageBuilder::makeIrcMessage( + &channel, privmsg, MessageParseArgs{}, originalMessage, 0); - auto msg = builder.build(); EXPECT_NE(msg.get(), nullptr); auto contextMap = buildContextMap(msg, &channel); From 867e3f3ab08ac8c7bd807f2fe8edfb1518ddcee5 Mon Sep 17 00:00:00 2001 From: nerix Date: Mon, 21 Oct 2024 00:57:37 +0200 Subject: [PATCH 23/40] fix: only invalidate buffers for chat windows (#5666) --- CHANGELOG.md | 2 +- src/widgets/BaseWindow.cpp | 11 +++++++---- src/widgets/BaseWindow.hpp | 1 + src/widgets/DraggablePopup.cpp | 6 +++--- src/widgets/OverlayWindow.cpp | 8 ++++++++ src/widgets/Window.cpp | 4 +++- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a08aac48..3555762ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Major: Add option to show pronouns in user card. (#5442, #5583) - Major: Release plugins alpha. (#5288) -- Major: Improve high-DPI support on Windows. (#4868, #5391, #5664) +- Major: Improve high-DPI support on Windows. (#4868, #5391, #5664, #5666) - Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746, #5643, #5659) - Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530) - Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625) diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index a82a364f2..31f72a3df 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -877,10 +877,13 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, break; case WM_DPICHANGED: { - // wait for Qt to process this message - postToThread([] { - getApp()->getWindows()->invalidateChannelViewBuffers(); - }); + if (this->flags_.has(ClearBuffersOnDpiChange)) + { + // wait for Qt to process this message + postToThread([] { + getApp()->getWindows()->invalidateChannelViewBuffers(); + }); + } } break; diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index 2f2168645..1f0455dce 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -37,6 +37,7 @@ public: Dialog = 1 << 6, DisableLayoutSave = 1 << 7, BoundsCheckOnShow = 1 << 8, + ClearBuffersOnDpiChange = 1 << 9, }; enum ActionOnFocusLoss { Nothing, Delete, Close, Hide }; diff --git a/src/widgets/DraggablePopup.cpp b/src/widgets/DraggablePopup.cpp index 84a57c0f7..6b7e9eaf9 100644 --- a/src/widgets/DraggablePopup.cpp +++ b/src/widgets/DraggablePopup.cpp @@ -36,9 +36,9 @@ namespace { DraggablePopup::DraggablePopup(bool closeAutomatically, QWidget *parent) : BaseWindow( - closeAutomatically - ? popupFlagsCloseAutomatically | BaseWindow::DisableLayoutSave - : popupFlags | BaseWindow::DisableLayoutSave, + (closeAutomatically ? popupFlagsCloseAutomatically : popupFlags) | + BaseWindow::DisableLayoutSave | + BaseWindow::ClearBuffersOnDpiChange, parent) , lifetimeHack_(std::make_shared(false)) , dragTimer_(this) diff --git a/src/widgets/OverlayWindow.cpp b/src/widgets/OverlayWindow.cpp index b6cff796e..ce7ee2489 100644 --- a/src/widgets/OverlayWindow.cpp +++ b/src/widgets/OverlayWindow.cpp @@ -7,6 +7,7 @@ #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" +#include "util/PostToThread.hpp" #include "widgets/BaseWidget.hpp" #include "widgets/helper/ChannelView.hpp" #include "widgets/helper/InvisibleSizeGrip.hpp" @@ -312,6 +313,13 @@ bool OverlayWindow::nativeEvent(const QByteArray &eventType, void *message, } break; # endif + case WM_DPICHANGED: { + // wait for Qt to process this message, same as in BaseWindow + postToThread([] { + getApp()->getWindows()->invalidateChannelViewBuffers(); + }); + } + break; default: return QWidget::nativeEvent(eventType, message, result); diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 4dcb14503..92b85c725 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -52,7 +52,9 @@ namespace chatterino { Window::Window(WindowType type, QWidget *parent) - : BaseWindow(BaseWindow::EnableCustomFrame, parent) + : BaseWindow( + {BaseWindow::EnableCustomFrame, BaseWindow::ClearBuffersOnDpiChange}, + parent) , type_(type) , notebook_(new SplitNotebook(this)) { From 45d2c292d02a498aaad9b4b4181df0b2b1611e89 Mon Sep 17 00:00:00 2001 From: pajlada Date: Mon, 21 Oct 2024 13:19:08 +0200 Subject: [PATCH 24/40] fix: subscribed threads not being marked as subscribed (#5668) --- CHANGELOG.md | 2 +- src/messages/MessageBuilder.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3555762ce..c95aa2df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,7 +110,7 @@ - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) - Dev: Move plugins to Sol2. (#5622) - Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652) -- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660) +- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660, #5668) - Dev: Refactored IRC message building. (#5663) ## 2.5.1 diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index db479adf3..a17f955a9 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -2382,6 +2382,11 @@ void MessageBuilder::parseThread(const QString &messageContent, this->message().replyParent = parent; thread->addToThread(std::weak_ptr{this->message_}); + if (thread->subscribed()) + { + this->message().flags.set(MessageFlag::SubscribedThread); + } + // enable reply flag this->message().flags.set(MessageFlag::ReplyMessage); From 2ec8fa8723e3e894f02dafc1c3065a2ce2765dd5 Mon Sep 17 00:00:00 2001 From: pajlada Date: Mon, 21 Oct 2024 19:22:23 +0200 Subject: [PATCH 25/40] refactor: remove unused ReplyContext.highlight (#5669) --- src/providers/twitch/IrcMessageHandler.cpp | 66 +++++++--------------- 1 file changed, 20 insertions(+), 46 deletions(-) diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index fb9d2fa13..0c345da83 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -123,47 +123,34 @@ int stripLeadingReplyMention(const QVariantMap &tags, QString &content) return 0; } -[[nodiscard]] bool shouldHighlightReplyThread( - const QVariantMap &tags, const QString &senderLogin, - std::shared_ptr &thread, bool isNew) +void checkThreadSubscription(const QVariantMap &tags, + const QString &senderLogin, + std::shared_ptr &thread) { - const auto ¤tLogin = - getApp()->getAccounts()->twitch.getCurrent()->getUserName(); - - if (thread->subscribed()) + if (thread->subscribed() || thread->unsubscribed()) { - return true; - } - - if (thread->unsubscribed()) - { - return false; + return; } if (getSettings()->autoSubToParticipatedThreads) { - if (isNew) - { - if (const auto it = tags.find("reply-parent-user-login"); - it != tags.end()) - { - auto name = it.value().toString(); - if (name == currentLogin) - { - thread->markSubscribed(); - return true; // already marked as participated - } - } - } + const auto ¤tLogin = + getApp()->getAccounts()->twitch.getCurrent()->getUserName(); if (senderLogin == currentLogin) { thread->markSubscribed(); - // don't set the highlight here + } + else if (const auto it = tags.find("reply-parent-user-login"); + it != tags.end()) + { + auto name = it.value().toString(); + if (name == currentLogin) + { + thread->markSubscribed(); + } } } - - return false; } ChannelPtr channelOrEmptyByTarget(const QString &target, @@ -243,7 +230,6 @@ QMap parseBadges(const QString &badgesString) struct ReplyContext { std::shared_ptr thread; MessagePtr parent; - bool highlight = false; }; [[nodiscard]] ReplyContext getReplyContext( @@ -265,8 +251,7 @@ struct ReplyContext { if (owned) { // Thread already exists (has a reply) - ctx.highlight = shouldHighlightReplyThread( - tags, message->nick(), owned, false); + checkThreadSubscription(tags, message->nick(), owned); ctx.thread = owned; rootThread = owned; } @@ -301,8 +286,7 @@ struct ReplyContext { { std::shared_ptr newThread = std::make_shared(foundMessage); - ctx.highlight = shouldHighlightReplyThread( - tags, message->nick(), newThread, true); + checkThreadSubscription(tags, message->nick(), newThread); ctx.thread = newThread; rootThread = newThread; @@ -724,10 +708,6 @@ std::vector IrcMessageHandler::parseMessageWithReply( if (built) { - if (replyCtx.highlight) - { - built->flags.set(MessageFlag::SubscribedThread); - } builtMessages.emplace_back(built); MessageBuilder::triggerHighlights(channel, alert); } @@ -1552,8 +1532,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, { // Thread already exists (has a reply) auto thread = threadIt->second.lock(); - replyCtx.highlight = shouldHighlightReplyThread( - tags, message->nick(), thread, false); + checkThreadSubscription(tags, message->nick(), thread); replyCtx.thread = thread; rootThread = thread; } @@ -1565,8 +1544,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, { // Found root reply message auto newThread = std::make_shared(root); - replyCtx.highlight = shouldHighlightReplyThread( - tags, message->nick(), newThread, true); + checkThreadSubscription(tags, message->nick(), newThread); replyCtx.thread = newThread; rootThread = newThread; @@ -1621,10 +1599,6 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, msg->flags.set(MessageFlag::Subscription); msg->flags.unset(MessageFlag::Highlighted); } - if (replyCtx.highlight) - { - msg->flags.set(MessageFlag::SubscribedThread); - } IrcMessageHandler::setSimilarityFlags(msg, chan); From 18c4815ad74bc794b952a3a787d59477952b91a7 Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:42:19 +0000 Subject: [PATCH 26/40] feat: add shared chat badge (#5661) --- CHANGELOG.md | 2 +- mocks/include/mocks/BaseApplication.hpp | 7 ++++ mocks/include/mocks/TwitchUsers.hpp | 24 ++++++++++++ resources/twitch/sharedChat.png | Bin 0 -> 1026 bytes src/messages/MessageBuilder.cpp | 35 ++++++++++++++++++ src/messages/MessageElement.hpp | 6 ++- src/singletons/WindowManager.cpp | 1 + src/widgets/helper/ChannelView.cpp | 5 +++ .../shared-chat-announcement.json | 18 +++++++++ .../IrcMessageHandler/shared-chat-emotes.json | 18 +++++++++ .../IrcMessageHandler/shared-chat-known.json | 18 +++++++++ .../shared-chat-unknown.json | 18 +++++++++ 12 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 mocks/include/mocks/TwitchUsers.hpp create mode 100644 resources/twitch/sharedChat.png diff --git a/CHANGELOG.md b/CHANGELOG.md index c95aa2df7..7497e46dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Major: Improve high-DPI support on Windows. (#4868, #5391, #5664, #5666) - Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746, #5643, #5659) - Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530) -- Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625) +- Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625, #5661) - Minor: Moved tab visibility control to a submenu, without any toggle actions. (#5530) - Minor: Add option to customise Moderation buttons with images. (#5369) - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) diff --git a/mocks/include/mocks/BaseApplication.hpp b/mocks/include/mocks/BaseApplication.hpp index 2ba9f949c..619203afc 100644 --- a/mocks/include/mocks/BaseApplication.hpp +++ b/mocks/include/mocks/BaseApplication.hpp @@ -3,6 +3,7 @@ #include "common/Args.hpp" #include "mocks/DisabledStreamerMode.hpp" #include "mocks/EmptyApplication.hpp" +#include "mocks/TwitchUsers.hpp" #include "providers/bttv/BttvLiveUpdates.hpp" #include "singletons/Fonts.hpp" #include "singletons/Settings.hpp" @@ -55,6 +56,11 @@ public: return &this->fonts; } + ITwitchUsers *getTwitchUsers() override + { + return &this->twitchUsers; + } + BttvLiveUpdates *getBttvLiveUpdates() override { return nullptr; @@ -71,6 +77,7 @@ public: DisabledStreamerMode streamerMode; Theme theme; Fonts fonts; + TwitchUsers twitchUsers; }; } // namespace chatterino::mock diff --git a/mocks/include/mocks/TwitchUsers.hpp b/mocks/include/mocks/TwitchUsers.hpp new file mode 100644 index 000000000..14f6bf7e6 --- /dev/null +++ b/mocks/include/mocks/TwitchUsers.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "providers/twitch/TwitchUser.hpp" +#include "providers/twitch/TwitchUsers.hpp" + +namespace chatterino::mock { + +class TwitchUsers : public ITwitchUsers +{ +public: + TwitchUsers() = default; + + std::shared_ptr resolveID(const UserId &id) + { + TwitchUser u = { + .id = id.string, + .name = {}, + .displayName = {}, + }; + return std::make_shared(u); + } +}; + +} // namespace chatterino::mock diff --git a/resources/twitch/sharedChat.png b/resources/twitch/sharedChat.png new file mode 100644 index 0000000000000000000000000000000000000000..f9a66b17c8f1307a811337d4095cfaf1d5133515 GIT binary patch literal 1026 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAk#;Q*fy*NU|NG(!LX|Nr^(e@*uP zd$<4p{rms?ng1U@{9iif|L@=buU-Cs`TYM?i~qlU^Z)U~|I@nvSEm0zxaa@IRsSb6 z|9}4M|DnDA`x^h>xcYzj{QnbM|9}7X|M;Q*i)Q|xH}(Il{{MA(|DQei-(LCu#q<=zrK9*{>1C^clU0Xn)vbhy!sVAg*UfUI2;J^GsxzPd&a=P zbkWnrF(l&f+Z#8_RvCz}J>ckI4mh6Gp1eq8rcA-QsO;bW{7YvAziCqwl*&8j`oBAl z;kEY6ZQZ^9?nM4g+q+Op)#OKEfM=le*R<1Bdz{PpV=u4BdMY%jF<|AI&}yKVL)iAq z*Rn-j#e%dR$gQXeipB1Fb4wiI2 z)>(dif#tKbx}Ez~3nu2QcM!HS*|~?m)L~YaVmrU($G6wNJY6cGRMTu|=CWKQEhf`u($Zu_j(ksaa11KN_h^Z&GV=o?ZM?@%@Y-&FVjXfAll)^oi9wdK|>goDW=gNNsxji~kMF<|(h3uuOo{TaS6teGKZnoa~OJoN)-YzWg&zh?9*|LBGHGu!>011cstd z7EdO$@Uf^g+_5=l@$$e`5tTVg`cFGl8s@Q{yFH<>LdZdwQRMIJ?w$$E4%%nV&s^u1 z=G3rk#(d$s`ZE<7I*)yGDlbU;!&35OowTjU;fv2t);@Yte7&cQq4VFy>Lo%p^SJJK zI2~3Euc}K_xyIP>?9VjsB-v$7^Y-YR^mnqe@L74c%_h^RTXkDcgRw__*SV&Nv%^kk zG&CBdvfN`gls~>aVLzvA((NOh=D#0&F;%tW>_5rgulTY3gV~p2*(FT#Ute&T;CII% zBV%e((49*^vNnknYTa7fH_LYKGA#v9%d6GQTYZ?NqBi;M%~e{R`fb5cHO8qcFTY&u zzFO4$?}U`}H{bT%eHpdo^>xE9O0o8L4SrvH93SN4boPM9;wQjdz~JfX=d#Wzp$Pys C^A0Qk literal 0 HcmV?d00001 diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index a17f955a9..601ba8ec9 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -32,6 +32,7 @@ #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrc.hpp" #include "providers/twitch/TwitchIrcServer.hpp" +#include "providers/twitch/TwitchUsers.hpp" #include "singletons/Emotes.hpp" #include "singletons/Resources.hpp" #include "singletons/Settings.hpp" @@ -380,6 +381,18 @@ EmotePtr makeAutoModBadge() Url{"https://dashboard.twitch.tv/settings/moderation/automod"}}); } +EmotePtr makeSharedChatBadge(const QString &sourceName) +{ + return std::make_shared(Emote{ + .name = EmoteName{}, + .images = ImageSet{Image::fromResourcePixmap( + getResources().twitch.sharedChat, 0.25)}, + .tooltip = Tooltip{"Shared Message" + + (sourceName.isEmpty() ? "" : " from " + sourceName)}, + .homePage = Url{"https://link.twitch.tv/SharedChatViewer"}, + }); +} + std::tuple, MessageElementFlags, bool> parseEmote( TwitchChannel *twitchChannel, const EmoteName &name) { @@ -2751,6 +2764,28 @@ void MessageBuilder::appendTwitchBadges(const QVariantMap &tags, return; } + if (this->message().flags.has(MessageFlag::SharedMessage)) + { + const QString sourceId = tags["source-room-id"].toString(); + QString sourceName; + if (sourceId.isEmpty()) + { + sourceName = ""; + } + else if (twitchChannel->roomId() == sourceId) + { + sourceName = twitchChannel->getName(); + } + else + { + sourceName = + getApp()->getTwitchUsers()->resolveID({sourceId})->displayName; + } + + this->emplace(makeSharedChatBadge(sourceName), + MessageElementFlag::BadgeSharedChannel); + } + auto badgeInfos = parseBadgeInfoTag(tags); auto badges = parseBadgeTag(tags); appendBadges(this, badges, badgeInfos, twitchChannel); diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 0e6c07b93..c19ed5c0c 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -66,6 +66,10 @@ enum class MessageElementFlag : int64_t { BitsStatic = (1LL << 11), BitsAnimated = (1LL << 12), + // Slot 0: Twitch + // - Shared Channel indicator badge + BadgeSharedChannel = (1LL << 37), + // Slot 1: Twitch // - Staff badge // - Admin badge @@ -119,7 +123,7 @@ enum class MessageElementFlag : int64_t { Badges = BadgeGlobalAuthority | BadgePredictions | BadgeChannelAuthority | BadgeSubscription | BadgeVanity | BadgeChatterino | BadgeSevenTV | - BadgeFfz, + BadgeFfz | BadgeSharedChannel, ChannelName = (1LL << 20), diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index f64eb47e7..88d5ec565 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -195,6 +195,7 @@ void WindowManager::updateWordTypeMask() flags.set(settings->animateEmotes ? MEF::BitsAnimated : MEF::BitsStatic); // badges + flags.set(MEF::BadgeSharedChannel); flags.set(settings->showBadgesGlobalAuthority ? MEF::BadgeGlobalAuthority : MEF::None); flags.set(settings->showBadgesPredictions ? MEF::BadgePredictions diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 066085030..50f6d1acc 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -2408,6 +2408,11 @@ void ChannelView::handleMouseClick(QMouseEvent *event, return; } + if (link.value.startsWith("id:")) + { + return; + } + // Insert @username into split input const bool commaMention = getSettings()->mentionUsersWithComma; diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json b/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json index 9c6fa2f65..a6877c365 100644 --- a/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json +++ b/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json @@ -64,6 +64,24 @@ "trailingSpace": true, "type": "TwitchModerationElement" }, + { + "emote": { + "homePage": "https://link.twitch.tv/SharedChatViewer", + "images": { + "1x": "" + }, + "name": "", + "tooltip": "Shared Message" + }, + "flags": "BadgeSharedChannel", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Shared Message", + "trailingSpace": true, + "type": "BadgeElement" + }, { "emote": { "homePage": "https://www.twitch.tv/jobs?ref=chat_badge", diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json b/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json index b7f9256d4..ba6485c21 100644 --- a/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json +++ b/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json @@ -64,6 +64,24 @@ "trailingSpace": true, "type": "TwitchModerationElement" }, + { + "emote": { + "homePage": "https://link.twitch.tv/SharedChatViewer", + "images": { + "1x": "" + }, + "name": "", + "tooltip": "Shared Message from twitchdev" + }, + "flags": "BadgeSharedChannel", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Shared Message from twitchdev", + "trailingSpace": true, + "type": "BadgeElement" + }, { "color": "#ffff0000", "flags": "Username", diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-known.json b/tests/snapshots/IrcMessageHandler/shared-chat-known.json index 6a13adcb4..c4a3f3e2f 100644 --- a/tests/snapshots/IrcMessageHandler/shared-chat-known.json +++ b/tests/snapshots/IrcMessageHandler/shared-chat-known.json @@ -64,6 +64,24 @@ "trailingSpace": true, "type": "TwitchModerationElement" }, + { + "emote": { + "homePage": "https://link.twitch.tv/SharedChatViewer", + "images": { + "1x": "" + }, + "name": "", + "tooltip": "Shared Message from twitchdev" + }, + "flags": "BadgeSharedChannel", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Shared Message from twitchdev", + "trailingSpace": true, + "type": "BadgeElement" + }, { "emote": { "homePage": "https://www.twitch.tv/jobs?ref=chat_badge", diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json b/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json index accbb76b0..6ea80b2ce 100644 --- a/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json +++ b/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json @@ -64,6 +64,24 @@ "trailingSpace": true, "type": "TwitchModerationElement" }, + { + "emote": { + "homePage": "https://link.twitch.tv/SharedChatViewer", + "images": { + "1x": "" + }, + "name": "", + "tooltip": "Shared Message" + }, + "flags": "BadgeSharedChannel", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Shared Message", + "trailingSpace": true, + "type": "BadgeElement" + }, { "emote": { "homePage": "https://www.twitch.tv/jobs?ref=chat_badge", From 55a12fa00838e7c6a859e14d388fe2bcb42bcfaf Mon Sep 17 00:00:00 2001 From: nerix Date: Tue, 22 Oct 2024 23:06:29 +0200 Subject: [PATCH 27/40] test: add snapshots for raids, timeouts, and notices (#5671) --- CHANGELOG.md | 2 +- .../IrcMessageHandler/clearchat.json | 157 ++++++++++++++++++ .../IrcMessageHandler/emoteonly-on.json | 157 ++++++++++++++++++ tests/snapshots/IrcMessageHandler/raid.json | 145 ++++++++++++++++ .../IrcMessageHandler/shared-chat-raid.json | 5 + .../snapshots/IrcMessageHandler/timeout.json | 87 ++++++++++ 6 files changed, 552 insertions(+), 1 deletion(-) create mode 100644 tests/snapshots/IrcMessageHandler/clearchat.json create mode 100644 tests/snapshots/IrcMessageHandler/emoteonly-on.json create mode 100644 tests/snapshots/IrcMessageHandler/raid.json create mode 100644 tests/snapshots/IrcMessageHandler/shared-chat-raid.json create mode 100644 tests/snapshots/IrcMessageHandler/timeout.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 7497e46dc..2992cd350 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,7 +104,7 @@ - Dev: Added more tests for input completion. (#5604) - Dev: Refactored legacy Unicode zero-width-joiner replacement. (#5594) - Dev: The JSON output when copying a message (SHIFT + right-click) is now more extensive. (#5600) -- Dev: Added more tests for message building. (#5598, #5654, #5656) +- Dev: Added more tests for message building. (#5598, #5654, #5656, #5671) - Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607) - Dev: `GIFTimer` is no longer initialized in tests. (#5608) - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) diff --git a/tests/snapshots/IrcMessageHandler/clearchat.json b/tests/snapshots/IrcMessageHandler/clearchat.json new file mode 100644 index 000000000..d40f9e704 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/clearchat.json @@ -0,0 +1,157 @@ +{ + "input": "@room-id=11148817;rm-received-ts=1729627607652;tmi-sent-ts=1729627607545;historical=1 :tmi.twitch.tv CLEARCHAT #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:06" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:06:47", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Chat" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "has" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "been" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cleared" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "by" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "moderator." + ] + } + ], + "flags": "System|DoNotTriggerNotification", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "Chat has been cleared by a moderator.", + "searchText": "Chat has been cleared by a moderator.", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/emoteonly-on.json b/tests/snapshots/IrcMessageHandler/emoteonly-on.json new file mode 100644 index 000000000..d3e0c4980 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/emoteonly-on.json @@ -0,0 +1,157 @@ +{ + "input": "@historical=1;rm-received-ts=1729627965650;msg-id=emote_only_on :tmi.twitch.tv NOTICE #pajlada :This room is now in emote-only mode.", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:12" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:12:45", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "This" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "room" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "is" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "now" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "in" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "emote-only" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "mode." + ] + } + ], + "flags": "System|DoNotTriggerNotification", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "This room is now in emote-only mode.", + "searchText": "This room is now in emote-only mode.", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/raid.json b/tests/snapshots/IrcMessageHandler/raid.json new file mode 100644 index 000000000..1e4e84d42 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/raid.json @@ -0,0 +1,145 @@ +{ + "input": "@badges=subscriber/24;login=nerixyz;msg-param-displayName=nerixyz;user-type=;tmi-sent-ts=1729626466361;system-msg=2\\sraiders\\sfrom\\snerixyz\\shave\\sjoined!;room-id=11148817;user-id=129546453;display-name=nerixyz;subscriber=1;historical=1;rm-received-ts=1729626466492;msg-id=raid;vip=0;id=7299b7bc-61ce-423c-85ce-8d651b56cce4;msg-param-login=nerixyz;color=#FF0000;mod=0;msg-param-viewerCount=2;flags=;msg-param-profileImageURL=https://static-cdn.jtvnw.net/jtv_user_pictures/e065218b-49df-459d-afd3-c6557870f551-profile_image-%s.png;emotes=;badge-info=subscriber/28 :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "19:47" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "19:47:46", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "raiders" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "from" + ] + }, + { + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "#ffff0000", + "userLoginName": "nerixyz", + "words": [ + "nerixyz" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "have" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "joined!" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "2 raiders from nerixyz have joined!", + "searchText": "2 raiders from nerixyz have joined!", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-raid.json b/tests/snapshots/IrcMessageHandler/shared-chat-raid.json new file mode 100644 index 000000000..d3edb3741 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/shared-chat-raid.json @@ -0,0 +1,5 @@ +{ + "input": "@color=#FF0000;emotes=;subscriber=0;msg-id=sharedchatnotice;historical=1;msg-param-profileImageURL=https://static-cdn.jtvnw.net/jtv_user_pictures/e065218b-49df-459d-afd3-c6557870f551-profile_image-%s.png;tmi-sent-ts=1729627237027;rm-received-ts=1729627237138;msg-param-displayName=nerixyz;id=c585cb3e-cb4f-4a48-a251-b568d217587e;display-name=nerixyz;badges=;user-id=129546453;source-id=d86cdfb2-e138-48e2-985f-5b8efb765ba4;source-room-id=955766119;room-id=11148817;user-type=;msg-param-login=nerixyz;flags=;source-badge-info=;mod=0;vip=0;system-msg=2\\sraiders\\sfrom\\snerixyz\\shave\\sjoined!;login=nerixyz;msg-param-viewerCount=2;source-badges=;source-msg-id=raid;badge-info= :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + ] +} diff --git a/tests/snapshots/IrcMessageHandler/timeout.json b/tests/snapshots/IrcMessageHandler/timeout.json new file mode 100644 index 000000000..1de84089d --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/timeout.json @@ -0,0 +1,87 @@ +{ + "input": "@tmi-sent-ts=1729628658012;rm-received-ts=1729628658106;historical=1;ban-duration=1;room-id=11148817;target-user-id=129546453 :tmi.twitch.tv CLEARCHAT #pajlada nerixyz", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:24" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:24:18", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "has", + "been", + "timed", + "out", + "for", + "1s." + ] + } + ], + "flags": "System|Timeout|DoNotTriggerNotification", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "nerixyz has been timed out for 1s. ", + "searchText": "nerixyz has been timed out for 1s. ", + "serverReceivedTime": "", + "timeoutUser": "nerixyz", + "usernameColor": "#ff000000" + } + ] +} From b46a893127bf0086d4e702a72e667e5a3fe3c984 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 21:49:58 +0000 Subject: [PATCH 28/40] chore(deps): bump lib/settings from `c58874c` to `4a0a1e5` (#5646) --- lib/settings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/settings b/lib/settings index c58874c1a..4a0a1e599 160000 --- a/lib/settings +++ b/lib/settings @@ -1 +1 @@ -Subproject commit c58874c1aa5d0619df2c975bcb87433941b46920 +Subproject commit 4a0a1e599377cdcdc91b0fbbefc312936b48730c From 9e8281b75b3ffddb6818b3dcf4c652fce78b1408 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 22:30:25 +0000 Subject: [PATCH 29/40] chore(deps): bump lib/expected-lite from `88ee08e` to `5b5caad` (#5667) --- lib/expected-lite | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/expected-lite b/lib/expected-lite index 88ee08eb3..5b5caad7c 160000 --- a/lib/expected-lite +++ b/lib/expected-lite @@ -1 +1 @@ -Subproject commit 88ee08eb3c3f3627ca54b90dafd1d63a6d4da96b +Subproject commit 5b5caad7cd57d5ba3ca796bf1521b131d73ca405 From f66bc37368f33a8c1983e64332118a3b92704fae Mon Sep 17 00:00:00 2001 From: pajlada Date: Thu, 24 Oct 2024 10:26:20 +0200 Subject: [PATCH 30/40] fix: use static version of 7tv badges (#5674) --- CHANGELOG.md | 1 + src/providers/seventv/SeventvBadges.cpp | 2 +- src/providers/seventv/SeventvEmotes.cpp | 37 +++++++++++++++++++------ src/providers/seventv/SeventvEmotes.hpp | 4 ++- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2992cd350..189ebadcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ - Bugfix: Fixed event emotes not showing up in autocomplete and popups. (#5239, #5580, #5582, #5632) - Bugfix: Fixed tab visibility being controllable in the emote popup. (#5530) - Bugfix: Fixed account switch not being saved if no other settings were changed. (#5558) +- Bugfix: Fixed 7TV badges being inadvertently animated. (#5674) - Bugfix: Fixed some tooltips not being readable. (#5578) - Bugfix: Fixed log files being locked longer than needed. (#5592) - Bugfix: Fixed global badges not showing in anonymous mode. (#5599) diff --git a/src/providers/seventv/SeventvBadges.cpp b/src/providers/seventv/SeventvBadges.cpp index 2f09bd087..e217fa914 100644 --- a/src/providers/seventv/SeventvBadges.cpp +++ b/src/providers/seventv/SeventvBadges.cpp @@ -59,7 +59,7 @@ void SeventvBadges::registerBadge(const QJsonObject &badgeJson) auto emote = Emote{ .name = EmoteName{}, - .images = SeventvEmotes::createImageSet(badgeJson), + .images = SeventvEmotes::createImageSet(badgeJson, true), .tooltip = Tooltip{badgeJson["tooltip"].toString()}, .homePage = Url{}, .id = EmoteId{badgeID}, diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index 969b0e5f5..3df627934 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -106,12 +106,18 @@ CreateEmoteResult createEmote(const QJsonObject &activeEmote, ? createAliasedTooltip(emoteName.string, baseEmoteName.string, author.string, isGlobal) : createTooltip(emoteName.string, author.string, isGlobal); - auto imageSet = SeventvEmotes::createImageSet(emoteData); + auto imageSet = SeventvEmotes::createImageSet(emoteData, false); - auto emote = - Emote({emoteName, imageSet, tooltip, - Url{EMOTE_LINK_FORMAT.arg(emoteId.string)}, zeroWidth, emoteId, - author, makeConditionedOptional(aliasedName, baseEmoteName)}); + auto emote = Emote({ + emoteName, + imageSet, + tooltip, + Url{EMOTE_LINK_FORMAT.arg(emoteId.string)}, + zeroWidth, + emoteId, + author, + makeConditionedOptional(aliasedName, baseEmoteName), + }); return {emote, emoteId, emoteName, !emote.images.getImage1()->isEmpty()}; } @@ -427,7 +433,8 @@ void SeventvEmotes::getEmoteSet( }); } -ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData) +ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData, + bool useStatic) { auto host = emoteData["host"].toObject(); // "//cdn.7tv[...]" @@ -463,9 +470,21 @@ ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData) baseWidth = width; } - auto image = Image::fromUrl( - {QString("https:%1/%2").arg(baseUrl, file["name"].toString())}, - scale, {static_cast(width), file["height"].toInt(16)}); + auto name = [&] { + if (useStatic) + { + auto staticName = file["static_name"].toString(); + if (!staticName.isEmpty()) + { + return staticName; + } + } + return file["name"].toString(); + }(); + + auto image = + Image::fromUrl({QString("https:%1/%2").arg(baseUrl, name)}, scale, + {static_cast(width), file["height"].toInt(16)}); sizes.at(nextSize) = image; nextSize++; diff --git a/src/providers/seventv/SeventvEmotes.hpp b/src/providers/seventv/SeventvEmotes.hpp index 03b966f9c..79a8500a9 100644 --- a/src/providers/seventv/SeventvEmotes.hpp +++ b/src/providers/seventv/SeventvEmotes.hpp @@ -153,8 +153,10 @@ public: * Creates an image set from a 7TV emote or badge. * * @param emoteData { host: { files: [], url } } + * @param useStatic use static version if possible */ - static ImageSet createImageSet(const QJsonObject &emoteData); + static ImageSet createImageSet(const QJsonObject &emoteData, + bool useStatic); private: Atomic> global_; From 74a385dfee4913da3bff63dfa998a5ccd1a922b7 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 27 Oct 2024 12:56:37 +0100 Subject: [PATCH 31/40] fix: escape 7TV emote names (#5677) --- CHANGELOG.md | 1 + src/providers/seventv/SeventvEmotes.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 189ebadcf..d11309f2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ - Bugfix: Fixed incorrect message being disabled in some cases upon approving or denying an automod caught message. (#5611) - Bugfix: Fixed double-click selection not working when clicking outside a message. (#5617) - Bugfix: Fixed emotes starting with ":" not tab-completing. (#5603) +- Bugfix: Fixed 7TV emotes messing with Qt's HTML. (#5677) - Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420) - Dev: Update vcpkg build Qt from 6.5.0 to 6.7.0, boost from 1.83.0 to 1.85.0, openssl from 3.1.3 to 3.3.0. (#5422) - Dev: Unsingletonize `ISoundController`. (#5462) diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index 3df627934..b19a61304 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -79,7 +79,8 @@ bool isZeroWidthRecommended(const QJsonObject &emoteData) Tooltip createTooltip(const QString &name, const QString &author, bool isGlobal) { return Tooltip{QString("%1
%2 7TV Emote
By: %3") - .arg(name, isGlobal ? "Global" : "Channel", + .arg(name.toHtmlEscaped(), + isGlobal ? "Global" : "Channel", author.isEmpty() ? "" : author)}; } @@ -87,7 +88,8 @@ Tooltip createAliasedTooltip(const QString &name, const QString &baseName, const QString &author, bool isGlobal) { return Tooltip{QString("%1
Alias of %2
%3 7TV Emote
By: %4") - .arg(name, baseName, isGlobal ? "Global" : "Channel", + .arg(name.toHtmlEscaped(), baseName.toHtmlEscaped(), + isGlobal ? "Global" : "Channel", author.isEmpty() ? "" : author)}; } From bbcd8c5eb2f2a3f6a6dfa8a8764ddd6457ed4648 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 27 Oct 2024 13:42:23 +0100 Subject: [PATCH 32/40] fix: get rid of some more warnings (#5672) --- CHANGELOG.md | 1 + mocks/include/mocks/Helix.hpp | 2 +- src/CMakeLists.txt | 1 + src/common/ProviderId.hpp | 4 +- src/common/SignalVector.hpp | 10 +++-- src/common/SignalVectorModel.hpp | 38 +++++++++++-------- .../completion/sources/UnifiedSource.cpp | 8 ++-- src/messages/LimitedQueue.hpp | 4 +- src/messages/MessageBuilder.cpp | 2 +- .../layouts/MessageLayoutContainer.cpp | 3 +- src/messages/layouts/MessageLayoutElement.cpp | 5 ++- src/providers/twitch/IrcMessageHandler.cpp | 2 +- src/providers/twitch/api/Helix.cpp | 6 +-- src/providers/twitch/api/Helix.hpp | 8 ++-- src/widgets/TooltipWidget.cpp | 4 +- src/widgets/dialogs/EditHotkeyDialog.cpp | 6 +-- src/widgets/dialogs/UserInfoPopup.cpp | 15 +++++--- src/widgets/helper/ChannelView.cpp | 16 ++++---- src/widgets/helper/SearchPopup.cpp | 4 +- src/widgets/listview/GenericListModel.cpp | 2 +- src/widgets/splits/SplitHeader.cpp | 2 +- 21 files changed, 79 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d11309f2a..a811097e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,6 +114,7 @@ - Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652) - Dev: Decoupled reply parsing from `MessageBuilder`. (#5660, #5668) - Dev: Refactored IRC message building. (#5663) +- Dev: Fixed some compiler warnings. (#5672) ## 2.5.1 diff --git a/mocks/include/mocks/Helix.hpp b/mocks/include/mocks/Helix.hpp index 5156de557..cfb2d71b6 100644 --- a/mocks/include/mocks/Helix.hpp +++ b/mocks/include/mocks/Helix.hpp @@ -350,7 +350,7 @@ public: // contains a comma MOCK_METHOD( void, getChatters, - (QString broadcasterID, QString moderatorID, int maxChattersToFetch, + (QString broadcasterID, QString moderatorID, size_t maxChattersToFetch, ResultCallback successCallback, (FailureCallback failureCallback)), (override)); // getChatters diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9d7134b97..bace57a9c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,6 +2,7 @@ set(LIBRARY_PROJECT "${PROJECT_NAME}-lib") set(VERSION_PROJECT "${LIBRARY_PROJECT}-version") set(EXECUTABLE_PROJECT "${PROJECT_NAME}") add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x050F00) +add_compile_definitions(QT_WARN_DEPRECATED_UP_TO=0x050F00) # registers the native messageing host option(CHATTERINO_DEBUG_NATIVE_MESSAGES "Debug native messages" OFF) diff --git a/src/common/ProviderId.hpp b/src/common/ProviderId.hpp index 18ccf8803..ccf997476 100644 --- a/src/common/ProviderId.hpp +++ b/src/common/ProviderId.hpp @@ -2,6 +2,8 @@ namespace chatterino { -enum class ProviderId { Twitch, Irc }; +enum class ProviderId { // NOLINT(performance-enum-size) + Twitch, +}; // } // namespace chatterino diff --git a/src/common/SignalVector.hpp b/src/common/SignalVector.hpp index e01ebd5aa..47d536cbe 100644 --- a/src/common/SignalVector.hpp +++ b/src/common/SignalVector.hpp @@ -87,7 +87,8 @@ public: } else { - assert(index >= 0 && index <= this->items_.size()); + assert(index >= 0 && + index <= static_cast(this->items_.size())); } this->items_.insert(this->items_.begin() + index, item); @@ -116,7 +117,7 @@ public: void removeAt(int index, void *caller = nullptr) { assertInGuiThread(); - assert(index >= 0 && index < int(this->items_.size())); + assert(index >= 0 && index < static_cast(this->items_.size())); T item = this->items_[index]; this->items_.erase(this->items_.begin() + index); @@ -132,13 +133,14 @@ public: { assertInGuiThread(); - for (int index = 0; index < this->items_.size(); ++index) + for (size_t index = 0; index < this->items_.size(); ++index) { T item = this->items_[index]; if (matcher(item)) { this->items_.erase(this->items_.begin() + index); - SignalVectorItemEvent args{item, index, caller}; + SignalVectorItemEvent args{item, static_cast(index), + caller}; this->itemRemoved.invoke(args); this->itemsChanged_(); return true; diff --git a/src/common/SignalVectorModel.hpp b/src/common/SignalVectorModel.hpp index 620ca452d..f7983288b 100644 --- a/src/common/SignalVectorModel.hpp +++ b/src/common/SignalVectorModel.hpp @@ -43,7 +43,7 @@ public: } // get row index int index = this->getModelIndexFromVectorIndex(args.index); - assert(index >= 0 && index <= this->rows_.size()); + assert(index >= 0 && index <= static_cast(this->rows_.size())); // get row items std::vector row = this->createRow(); @@ -75,7 +75,7 @@ public: } int row = this->getModelIndexFromVectorIndex(args.index); - assert(row >= 0 && row <= this->rows_.size()); + assert(row >= 0 && row <= static_cast(this->rows_.size())); // remove row std::vector items = this->rows_[row].items; @@ -130,7 +130,8 @@ public: { int row = index.row(); int column = index.column(); - if (row < 0 || column < 0 || row >= this->rows_.size() || + if (row < 0 || column < 0 || + row >= static_cast(this->rows_.size()) || column >= this->columnCount_) { return QVariant(); @@ -144,7 +145,8 @@ public: { int row = index.row(); int column = index.column(); - if (row < 0 || column < 0 || row >= this->rows_.size() || + if (row < 0 || column < 0 || + row >= static_cast(this->rows_.size()) || column >= this->columnCount_) { return false; @@ -152,7 +154,7 @@ public: Row &rowItem = this->rows_[row]; - assert(this->columnCount_ == rowItem.items.size()); + assert(this->columnCount_ == static_cast(rowItem.items.size())); auto &cell = rowItem.items[column]; @@ -167,7 +169,7 @@ public: int vecRow = this->getVectorIndexFromModelIndex(row); // TODO: This is only a safety-thing for when we modify data that's being modified right now. // It should not be necessary, but it would require some rethinking about this surrounding logic - if (vecRow >= this->vector_->readOnly()->size()) + if (vecRow >= static_cast(this->vector_->readOnly()->size())) { return false; } @@ -224,18 +226,19 @@ public: { int row = index.row(), column = index.column(); - if (row < 0 || column < 0 || row >= this->rows_.size() || + if (row < 0 || column < 0 || + row >= static_cast(this->rows_.size()) || column >= this->columnCount_) { return Qt::NoItemFlags; } - assert(row >= 0 && row < this->rows_.size() && column >= 0 && - column < this->columnCount_); + assert(row >= 0 && row < static_cast(this->rows_.size()) && + column >= 0 && column < this->columnCount_); const auto &rowItem = this->rows_[row]; - assert(this->columnCount_ == rowItem.items.size()); + assert(this->columnCount_ == static_cast(rowItem.items.size())); return rowItem.items[column]->flags(); } @@ -267,7 +270,8 @@ public: return false; } - assert(sourceRow >= 0 && sourceRow < this->rows_.size()); + assert(sourceRow >= 0 && + sourceRow < static_cast(this->rows_.size())); int signalVectorRow = this->getVectorIndexFromModelIndex(sourceRow); this->beginMoveRows(sourceParent, sourceRow, sourceRow, @@ -294,7 +298,7 @@ public: return false; } - assert(row >= 0 && row < this->rows_.size()); + assert(row >= 0 && row < static_cast(this->rows_.size())); int signalVectorRow = this->getVectorIndexFromModelIndex(row); this->vector_->removeAt(signalVectorRow); @@ -337,8 +341,10 @@ public: int from = data->data("chatterino_row_id").toInt(); int to = parent.row(); - int vectorFrom = this->getVectorIndexFromModelIndex(from); - int vectorTo = this->getVectorIndexFromModelIndex(to); + auto vectorFrom = + static_cast(this->getVectorIndexFromModelIndex(from)); + auto vectorTo = + static_cast(this->getVectorIndexFromModelIndex(to)); if (vectorFrom < 0 || vectorFrom > this->vector_->raw().size() || vectorTo < 0 || vectorTo > this->vector_->raw().size()) @@ -402,7 +408,7 @@ protected: void insertCustomRow(std::vector row, int index) { - assert(index >= 0 && index <= this->rows_.size()); + assert(index >= 0 && index <= static_cast(this->rows_.size())); this->beginInsertRows(QModelIndex(), index, index); this->rows_.insert(this->rows_.begin() + index, @@ -412,7 +418,7 @@ protected: void removeCustomRow(int index) { - assert(index >= 0 && index <= this->rows_.size()); + assert(index >= 0 && index <= static_cast(this->rows_.size())); assert(this->rows_[index].isCustomRow); this->beginRemoveRows(QModelIndex(), index, index); diff --git a/src/controllers/completion/sources/UnifiedSource.cpp b/src/controllers/completion/sources/UnifiedSource.cpp index a0f462ace..6d8dfe15d 100644 --- a/src/controllers/completion/sources/UnifiedSource.cpp +++ b/src/controllers/completion/sources/UnifiedSource.cpp @@ -37,7 +37,7 @@ void UnifiedSource::addToListModel(GenericListModel &model, source->addToListModel(model, maxCount - used); // Calculate how many items have been added so far used = model.rowCount() - startingSize; - if (used >= maxCount) + if (used >= static_cast(maxCount)) { // Used up all of limit break; @@ -58,15 +58,15 @@ void UnifiedSource::addToStringList(QStringList &list, size_t maxCount, } // Make sure to only add maxCount elements in total. - int startingSize = list.size(); - int used = 0; + auto startingSize = list.size(); + QStringList::size_type used = 0; for (const auto &source : this->sources_) { source->addToStringList(list, maxCount - used, isFirstWord); // Calculate how many items have been added so far used = list.size() - startingSize; - if (used >= maxCount) + if (used >= static_cast(maxCount)) { // Used up all of limit break; diff --git a/src/messages/LimitedQueue.hpp b/src/messages/LimitedQueue.hpp index e06e5a0f2..a204c84a9 100644 --- a/src/messages/LimitedQueue.hpp +++ b/src/messages/LimitedQueue.hpp @@ -196,12 +196,12 @@ public: std::unique_lock lock(this->mutex_); Equals eq; - for (int i = 0; i < this->buffer_.size(); ++i) + for (size_t i = 0; i < this->buffer_.size(); ++i) { if (eq(this->buffer_[i], needle)) { this->buffer_[i] = replacement; - return i; + return static_cast(i); } } return -1; diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 601ba8ec9..cee5a7c87 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -116,7 +116,7 @@ QString formatUpdatedEmoteList(const QString &platform, text += QString(" %1 %2 emotes ").arg(emoteNames.size()).arg(platform); } - auto i = 0; + size_t i = 0; for (const auto &emoteName : emoteNames) { i++; diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index 0e84d3252..dbf04fb77 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -212,7 +212,8 @@ void MessageLayoutContainer::breakLine() this->lineStart_ = this->elements_.size(); // this->currentX = (int)(this->scale * 8); - if (this->canCollapse() && this->line_ + 1 >= maxUncollapsedLines()) + if (this->canCollapse() && + static_cast(this->line_ + 1) >= maxUncollapsedLines()) { this->canAddMessages_ = false; return; diff --git a/src/messages/layouts/MessageLayoutElement.cpp b/src/messages/layouts/MessageLayoutElement.cpp index 7b5e3fb58..0031348d6 100644 --- a/src/messages/layouts/MessageLayoutElement.cpp +++ b/src/messages/layouts/MessageLayoutElement.cpp @@ -514,9 +514,10 @@ int TextLayoutElement::getXFromIndex(size_t index) else if (index < static_cast(this->getText().size())) { int x = 0; - for (int i = 0; i < index; i++) + for (size_t i = 0; i < index; i++) { - x += metrics.horizontalAdvance(this->getText()[i]); + x += metrics.horizontalAdvance( + this->getText()[static_cast(i)]); } return x + this->getRect().left(); } diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 0c345da83..249bd6c3c 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -1411,7 +1411,7 @@ float IrcMessageHandler::similarity( float similarityPercent = 0.0F; int checked = 0; - for (int i = 1; i <= messages.size(); ++i) + for (size_t i = 1; i <= messages.size(); ++i) { if (checked >= getSettings()->hideSimilarMaxMessagesToCheck) { diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index a756a0f47..593c6837f 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -1909,7 +1909,7 @@ void Helix::updateChatSettings( void Helix::onFetchChattersSuccess( std::shared_ptr finalChatters, QString broadcasterID, - QString moderatorID, int maxChattersToFetch, + QString moderatorID, size_t maxChattersToFetch, ResultCallback successCallback, FailureCallback failureCallback, HelixChatters chatters) @@ -2022,7 +2022,7 @@ void Helix::fetchChatters( void Helix::onFetchModeratorsSuccess( std::shared_ptr> finalModerators, - QString broadcasterID, int maxModeratorsToFetch, + QString broadcasterID, size_t maxModeratorsToFetch, ResultCallback> successCallback, FailureCallback failureCallback, HelixModerators moderators) @@ -2459,7 +2459,7 @@ void Helix::sendWhisper( // https://dev.twitch.tv/docs/api/reference#get-chatters void Helix::getChatters( - QString broadcasterID, QString moderatorID, int maxChattersToFetch, + QString broadcasterID, QString moderatorID, size_t maxChattersToFetch, ResultCallback successCallback, FailureCallback failureCallback) { diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index ec82f1654..3a81a9908 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -1073,7 +1073,7 @@ public: // This will follow the returned cursor and return up to `maxChattersToFetch` chatters // https://dev.twitch.tv/docs/api/reference#get-chatters virtual void getChatters( - QString broadcasterID, QString moderatorID, int maxChattersToFetch, + QString broadcasterID, QString moderatorID, size_t maxChattersToFetch, ResultCallback successCallback, FailureCallback failureCallback) = 0; @@ -1417,7 +1417,7 @@ public: // This will follow the returned cursor and return up to `maxChattersToFetch` chatters // https://dev.twitch.tv/docs/api/reference#get-chatters void getChatters( - QString broadcasterID, QString moderatorID, int maxChattersToFetch, + QString broadcasterID, QString moderatorID, size_t maxChattersToFetch, ResultCallback successCallback, FailureCallback failureCallback) final; @@ -1505,7 +1505,7 @@ protected: // Recursive boy void onFetchChattersSuccess( std::shared_ptr finalChatters, QString broadcasterID, - QString moderatorID, int maxChattersToFetch, + QString moderatorID, size_t maxChattersToFetch, ResultCallback successCallback, FailureCallback failureCallback, HelixChatters chatters); @@ -1520,7 +1520,7 @@ protected: // Recursive boy void onFetchModeratorsSuccess( std::shared_ptr> finalModerators, - QString broadcasterID, int maxModeratorsToFetch, + QString broadcasterID, size_t maxModeratorsToFetch, ResultCallback> successCallback, FailureCallback failureCallback, HelixModerators moderators); diff --git a/src/widgets/TooltipWidget.cpp b/src/widgets/TooltipWidget.cpp index 64b3bec82..266b2ea03 100644 --- a/src/widgets/TooltipWidget.cpp +++ b/src/widgets/TooltipWidget.cpp @@ -119,9 +119,9 @@ void TooltipWidget::set(const std::vector &entries, this->setVisibleEntries(entries.size()); - for (int i = 0; i < entries.size(); ++i) + for (size_t i = 0; i < entries.size(); ++i) { - if (auto *entryWidget = this->entryAt(i)) + if (auto *entryWidget = this->entryAt(static_cast(i))) { const auto &entry = entries[i]; entryWidget->setImage(entry.image); diff --git a/src/widgets/dialogs/EditHotkeyDialog.cpp b/src/widgets/dialogs/EditHotkeyDialog.cpp index 3f7c61242..0749e4749 100644 --- a/src/widgets/dialogs/EditHotkeyDialog.cpp +++ b/src/widgets/dialogs/EditHotkeyDialog.cpp @@ -79,7 +79,7 @@ void EditHotkeyDialog::setFromHotkey(std::shared_ptr hotkey) this->ui_->easyArgsLabel->setText(def->argumentsPrompt); this->ui_->easyArgsLabel->setToolTip(def->argumentsPromptHover); int matchIdx = -1; - for (int i = 0; i < def->possibleArguments.size(); i++) + for (size_t i = 0; i < def->possibleArguments.size(); i++) { const auto &[displayText, argData] = def->possibleArguments.at(i); this->ui_->easyArgsPicker->addItem(displayText); @@ -90,7 +90,7 @@ void EditHotkeyDialog::setFromHotkey(std::shared_ptr hotkey) continue; } bool matches = true; - for (int j = 0; j < argData.size(); j++) + for (size_t j = 0; j < argData.size(); j++) { if (argData.at(j) != hotkey->arguments().at(j)) { @@ -100,7 +100,7 @@ void EditHotkeyDialog::setFromHotkey(std::shared_ptr hotkey) } if (matches) { - matchIdx = i; + matchIdx = static_cast(i); } } if (matchIdx != -1) diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index fabae271d..f8bec9b1b 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -219,11 +219,13 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, Split *split) const auto &timeoutButtons = getSettings()->timeoutButtons.getValue(); - if (timeoutButtons.size() < buttonNum || 0 >= buttonNum) + if (static_cast(timeoutButtons.size()) < buttonNum || + 0 >= buttonNum) { return QString("Invalid argument for execModeratorAction: " "%1. Integer out of usable range: [1, %2]") - .arg(buttonNum, timeoutButtons.size() - 1); + .arg(buttonNum, + static_cast(timeoutButtons.size()) - 1); } const auto &button = timeoutButtons.at(buttonNum - 1); msg = QString("/timeout %1 %2") @@ -690,9 +692,10 @@ void UserInfoPopup::installEvents() { const auto &vector = getSettings()->blacklistedUsers.raw(); - for (int i = 0; i < vector.size(); i++) + for (int i = 0; i < static_cast(vector.size()); i++) { - if (this->userName_ == vector[i].getPattern()) + if (this->userName_ == + vector[static_cast(i)].getPattern()) { getSettings()->blacklistedUsers.removeAt(i); i--; @@ -899,9 +902,9 @@ void UserInfoPopup::updateUserData() // get ignoreHighlights state bool isIgnoringHighlights = false; const auto &vector = getSettings()->blacklistedUsers.raw(); - for (int i = 0; i < vector.size(); i++) + for (const auto &user : vector) { - if (this->userName_ == vector[i].getPattern()) + if (this->userName_ == user.getPattern()) { isIgnoringHighlights = true; break; diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 50f6d1acc..dc0a98cbd 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -2204,8 +2204,8 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) { this->isDoubleClick_ = false; // Was actually not a wanted triple-click - if (fabsf(distanceBetweenPoints(this->lastDoubleClickPosition_, - event->screenPos())) > 10.F) + if (std::abs(distanceBetweenPoints(this->lastDoubleClickPosition_, + event->screenPos())) > 10.F) { this->clickTimer_.stop(); return; @@ -2215,16 +2215,16 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) { this->isLeftMouseDown_ = false; - if (fabsf(distanceBetweenPoints(this->lastLeftPressPosition_, - event->screenPos())) > 15.F) + if (std::abs(distanceBetweenPoints(this->lastLeftPressPosition_, + event->screenPos())) > 15.F) { return; } // Triple-clicking a message selects the whole message if (foundElement && this->clickTimer_.isActive() && - (fabsf(distanceBetweenPoints(this->lastDoubleClickPosition_, - event->screenPos())) < 10.F)) + (std::abs(distanceBetweenPoints(this->lastDoubleClickPosition_, + event->screenPos())) < 10.F)) { this->selectWholeMessage(layout.get(), messageIndex); return; @@ -2241,8 +2241,8 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) { this->isRightMouseDown_ = false; - if (fabsf(distanceBetweenPoints(this->lastRightPressPosition_, - event->screenPos())) > 15.F) + if (std::abs(distanceBetweenPoints(this->lastRightPressPosition_, + event->screenPos())) > 15.F) { return; } diff --git a/src/widgets/helper/SearchPopup.cpp b/src/widgets/helper/SearchPopup.cpp index ff7e08816..73f6c47a4 100644 --- a/src/widgets/helper/SearchPopup.cpp +++ b/src/widgets/helper/SearchPopup.cpp @@ -241,10 +241,8 @@ LimitedQueueSnapshot SearchPopup::buildSnapshot() const LimitedQueueSnapshot &snapshot = sharedView.channel()->getMessageSnapshot(); - // TODO: implement iterator on LimitedQueueSnapshot? - for (auto i = 0; i < snapshot.size(); ++i) + for (const auto &message : snapshot) { - const MessagePtr &message = snapshot[i]; if (filterSet && !filterSet->filter(message, sharedView.channel())) { continue; diff --git a/src/widgets/listview/GenericListModel.cpp b/src/widgets/listview/GenericListModel.cpp index 0bb111170..a76e6124e 100644 --- a/src/widgets/listview/GenericListModel.cpp +++ b/src/widgets/listview/GenericListModel.cpp @@ -19,7 +19,7 @@ QVariant GenericListModel::data(const QModelIndex &index, int /* role */) const return {}; } - if (index.row() >= this->items_.size()) + if (index.row() >= static_cast(this->items_.size())) { return {}; } diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index b82f817be..f1f84eff4 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -42,7 +42,7 @@ namespace { using namespace chatterino; // 5 minutes -constexpr const uint64_t THUMBNAIL_MAX_AGE_MS = 5ULL * 60 * 1000; +constexpr const qint64 THUMBNAIL_MAX_AGE_MS = 5LL * 60 * 1000; auto formatRoomModeUnclean( const SharedAccessGuard &modes) -> QString From 90211cca556ae8a32f043ed48ca0d60f766efa85 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 27 Oct 2024 14:10:52 +0100 Subject: [PATCH 33/40] fix(cmake): use boost's own cmake config file (#5679) --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 023135891..603c9c42b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.15) cmake_policy(SET CMP0087 NEW) # evaluates generator expressions in `install(CODE/SCRIPT)` cmake_policy(SET CMP0091 NEW) # select MSVC runtime library through `CMAKE_MSVC_RUNTIME_LIBRARY` +if (POLICY CMP0167) + cmake_policy(SET CMP0167 NEW) # find Boost's own CMake config file +endif () include(FeatureSummary) list(APPEND CMAKE_MODULE_PATH From ecfb35c9b7d25883434ecd40dbc349a511e0a7fe Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 2 Nov 2024 12:22:17 +0100 Subject: [PATCH 34/40] fix(luals-meta): Use opaque enum values and correct HTTP types (#5682) --- CHANGELOG.md | 4 +- docs/plugin-meta.lua | 115 ++++++++++--------- scripts/make_luals_meta.py | 23 ++-- src/common/network/NetworkCommon.hpp | 2 +- src/controllers/plugins/api/HTTPRequest.hpp | 30 ++--- src/controllers/plugins/api/HTTPResponse.hpp | 18 ++- 6 files changed, 101 insertions(+), 91 deletions(-) mode change 100644 => 100755 scripts/make_luals_meta.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a811097e2..6903bfc72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,7 +75,7 @@ - Dev: Removed unused timegate settings. (#5361) - Dev: Add `Channel::addSystemMessage` helper function, allowing us to avoid the common `channel->addMessage(makeSystemMessage(...));` pattern. (#5500) - Dev: Unsingletonize `Resources2`. (#5460) -- Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385) +- Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385, #5682) - Dev: Images are now loaded in worker threads. (#5431) - Dev: Fixed broken `SignalVector::operator[]` implementation. (#5556) - Dev: Qt Creator now auto-configures Conan when loading the project and skips vcpkg. (#5305) @@ -110,7 +110,7 @@ - Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607) - Dev: `GIFTimer` is no longer initialized in tests. (#5608) - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) -- Dev: Move plugins to Sol2. (#5622) +- Dev: Move plugins to Sol2. (#5622, #5682) - Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652) - Dev: Decoupled reply parsing from `MessageBuilder`. (#5660, #5668) - Dev: Refactored IRC message building. (#5663) diff --git a/docs/plugin-meta.lua b/docs/plugin-meta.lua index 5a86efca0..27cdf8786 100644 --- a/docs/plugin-meta.lua +++ b/docs/plugin-meta.lua @@ -5,20 +5,20 @@ -- Add the folder this file is in to "Lua.workspace.library". c2 = {} ----@alias c2.LogLevel.Debug "c2.LogLevel.Debug" ----@alias c2.LogLevel.Info "c2.LogLevel.Info" ----@alias c2.LogLevel.Warning "c2.LogLevel.Warning" ----@alias c2.LogLevel.Critical "c2.LogLevel.Critical" ----@alias c2.LogLevel c2.LogLevel.Debug|c2.LogLevel.Info|c2.LogLevel.Warning|c2.LogLevel.Critical ----@type { Debug: c2.LogLevel.Debug, Info: c2.LogLevel.Info, Warning: c2.LogLevel.Warning, Critical: c2.LogLevel.Critical } -c2.LogLevel = {} +---@enum c2.LogLevel +c2.LogLevel = { + Debug = {}, ---@type c2.LogLevel.Debug + Info = {}, ---@type c2.LogLevel.Info + Warning = {}, ---@type c2.LogLevel.Warning + Critical = {}, ---@type c2.LogLevel.Critical +} -- Begin src/controllers/plugins/api/EventType.hpp ----@alias c2.EventType.CompletionRequested "c2.EventType.CompletionRequested" ----@alias c2.EventType c2.EventType.CompletionRequested ----@type { CompletionRequested: c2.EventType.CompletionRequested } -c2.EventType = {} +---@enum c2.EventType +c2.EventType = { + CompletionRequested = {}, ---@type c2.EventType.CompletionRequested +} -- End src/controllers/plugins/api/EventType.hpp @@ -38,19 +38,19 @@ c2.EventType = {} -- Begin src/common/Channel.hpp ----@alias c2.ChannelType.None "c2.ChannelType.None" ----@alias c2.ChannelType.Direct "c2.ChannelType.Direct" ----@alias c2.ChannelType.Twitch "c2.ChannelType.Twitch" ----@alias c2.ChannelType.TwitchWhispers "c2.ChannelType.TwitchWhispers" ----@alias c2.ChannelType.TwitchWatching "c2.ChannelType.TwitchWatching" ----@alias c2.ChannelType.TwitchMentions "c2.ChannelType.TwitchMentions" ----@alias c2.ChannelType.TwitchLive "c2.ChannelType.TwitchLive" ----@alias c2.ChannelType.TwitchAutomod "c2.ChannelType.TwitchAutomod" ----@alias c2.ChannelType.TwitchEnd "c2.ChannelType.TwitchEnd" ----@alias c2.ChannelType.Misc "c2.ChannelType.Misc" ----@alias c2.ChannelType c2.ChannelType.None|c2.ChannelType.Direct|c2.ChannelType.Twitch|c2.ChannelType.TwitchWhispers|c2.ChannelType.TwitchWatching|c2.ChannelType.TwitchMentions|c2.ChannelType.TwitchLive|c2.ChannelType.TwitchAutomod|c2.ChannelType.TwitchEnd|c2.ChannelType.Misc ----@type { None: c2.ChannelType.None, Direct: c2.ChannelType.Direct, Twitch: c2.ChannelType.Twitch, TwitchWhispers: c2.ChannelType.TwitchWhispers, TwitchWatching: c2.ChannelType.TwitchWatching, TwitchMentions: c2.ChannelType.TwitchMentions, TwitchLive: c2.ChannelType.TwitchLive, TwitchAutomod: c2.ChannelType.TwitchAutomod, TwitchEnd: c2.ChannelType.TwitchEnd, Misc: c2.ChannelType.Misc } -c2.ChannelType = {} +---@enum c2.ChannelType +c2.ChannelType = { + None = {}, ---@type c2.ChannelType.None + Direct = {}, ---@type c2.ChannelType.Direct + Twitch = {}, ---@type c2.ChannelType.Twitch + TwitchWhispers = {}, ---@type c2.ChannelType.TwitchWhispers + TwitchWatching = {}, ---@type c2.ChannelType.TwitchWatching + TwitchMentions = {}, ---@type c2.ChannelType.TwitchMentions + TwitchLive = {}, ---@type c2.ChannelType.TwitchLive + TwitchAutomod = {}, ---@type c2.ChannelType.TwitchAutomod + TwitchEnd = {}, ---@type c2.ChannelType.TwitchEnd + Misc = {}, ---@type c2.ChannelType.Misc +} -- End src/common/Channel.hpp @@ -174,90 +174,97 @@ function c2.Channel.by_twitch_id(id) end -- Begin src/controllers/plugins/api/HTTPResponse.hpp ----@class HTTPResponse -HTTPResponse = {} +---@class c2.HTTPResponse +c2.HTTPResponse = {} --- Returns the data. This is not guaranteed to be encoded using any --- particular encoding scheme. It's just the bytes the server returned. --- -function HTTPResponse:data() end +---@return string +---@nodiscard +function c2.HTTPResponse:data() end --- Returns the status code. --- -function HTTPResponse:status() end +---@return number|nil +---@nodiscard +function c2.HTTPResponse:status() end --- A somewhat human readable description of an error if such happened --- -function HTTPResponse:error() end +---@return string +---@nodiscard +function c2.HTTPResponse:error() end ---@return string -function HTTPResponse:__tostring() end +---@nodiscard +function c2.HTTPResponse:__tostring() end -- End src/controllers/plugins/api/HTTPResponse.hpp -- Begin src/controllers/plugins/api/HTTPRequest.hpp ----@alias HTTPCallback fun(result: HTTPResponse): nil ----@class HTTPRequest -HTTPRequest = {} +---@alias c2.HTTPCallback fun(result: c2.HTTPResponse): nil +---@class c2.HTTPRequest +c2.HTTPRequest = {} --- Sets the success callback --- ----@param callback HTTPCallback Function to call when the HTTP request succeeds -function HTTPRequest:on_success(callback) end +---@param callback c2.HTTPCallback Function to call when the HTTP request succeeds +function c2.HTTPRequest:on_success(callback) end --- Sets the failure callback --- ----@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status -function HTTPRequest:on_error(callback) end +---@param callback c2.HTTPCallback Function to call when the HTTP request fails or returns a non-ok status +function c2.HTTPRequest:on_error(callback) end --- Sets the finally callback --- ---@param callback fun(): nil Function to call when the HTTP request finishes -function HTTPRequest:finally(callback) end +function c2.HTTPRequest:finally(callback) end --- Sets the timeout --- ---@param timeout integer How long in milliseconds until the times out -function HTTPRequest:set_timeout(timeout) end +function c2.HTTPRequest:set_timeout(timeout) end --- Sets the request payload --- ---@param data string -function HTTPRequest:set_payload(data) end +function c2.HTTPRequest:set_payload(data) end --- Sets a header in the request --- ---@param name string ---@param value string -function HTTPRequest:set_header(name, value) end +function c2.HTTPRequest:set_header(name, value) end --- Executes the HTTP request --- -function HTTPRequest:execute() end +function c2.HTTPRequest:execute() end ---@return string -function HTTPRequest:__tostring() end +function c2.HTTPRequest:__tostring() end --- Creates a new HTTPRequest --- ----@param method HTTPMethod Method to use +---@param method c2.HTTPMethod Method to use ---@param url string Where to send the request to ----@return HTTPRequest -function HTTPRequest.create(method, url) end +---@return c2.HTTPRequest +function c2.HTTPRequest.create(method, url) end -- End src/controllers/plugins/api/HTTPRequest.hpp -- Begin src/common/network/NetworkCommon.hpp ----@alias HTTPMethod.Get "HTTPMethod.Get" ----@alias HTTPMethod.Post "HTTPMethod.Post" ----@alias HTTPMethod.Put "HTTPMethod.Put" ----@alias HTTPMethod.Delete "HTTPMethod.Delete" ----@alias HTTPMethod.Patch "HTTPMethod.Patch" ----@alias HTTPMethod HTTPMethod.Get|HTTPMethod.Post|HTTPMethod.Put|HTTPMethod.Delete|HTTPMethod.Patch ----@type { Get: HTTPMethod.Get, Post: HTTPMethod.Post, Put: HTTPMethod.Put, Delete: HTTPMethod.Delete, Patch: HTTPMethod.Patch } -HTTPMethod = {} +---@enum c2.HTTPMethod +c2.HTTPMethod = { + Get = {}, ---@type c2.HTTPMethod.Get + Post = {}, ---@type c2.HTTPMethod.Post + Put = {}, ---@type c2.HTTPMethod.Put + Delete = {}, ---@type c2.HTTPMethod.Delete + Patch = {}, ---@type c2.HTTPMethod.Patch +} -- End src/common/network/NetworkCommon.hpp diff --git a/scripts/make_luals_meta.py b/scripts/make_luals_meta.py old mode 100644 new mode 100755 index b1420e780..e1dafe496 --- a/scripts/make_luals_meta.py +++ b/scripts/make_luals_meta.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 """ This script generates docs/plugin-meta.lua. It accepts no arguments @@ -242,25 +243,19 @@ def read_file(path: Path, out: TextIOWrapper): ) name = header[0].split(" ", 1)[1] printmsg(path, reader.line_no(), f"enum {name}") - variants = reader.read_enum_variants() - - vtypes = [] - for variant in variants: - vtype = f'{name}.{variant}' - vtypes.append(vtype) - out.write(f'---@alias {vtype} "{vtype}"\n') - - out.write(f"---@alias {name} {'|'.join(vtypes)}\n") if header_comment: out.write(f"--- {header_comment}\n") - out.write("---@type { ") + out.write(f"---@enum {name}\n") + out.write(f"{name} = {{\n") out.write( - ", ".join( - [f"{variant}: {typ}" for variant, typ in zip(variants,vtypes)] + "\n".join( + [ + f" {variant} = {{}}, ---@type {name}.{variant}" + for variant in reader.read_enum_variants() + ] ) ) - out.write(" }\n") - out.write(f"{name} = {{}}\n\n") + out.write("\n}\n\n") continue # class diff --git a/src/common/network/NetworkCommon.hpp b/src/common/network/NetworkCommon.hpp index 215b828a7..8efd66620 100644 --- a/src/common/network/NetworkCommon.hpp +++ b/src/common/network/NetworkCommon.hpp @@ -16,7 +16,7 @@ using NetworkErrorCallback = std::function; using NetworkFinallyCallback = std::function; /** - * @exposeenum HTTPMethod + * @exposeenum c2.HTTPMethod */ enum class NetworkRequestType { Get, diff --git a/src/controllers/plugins/api/HTTPRequest.hpp b/src/controllers/plugins/api/HTTPRequest.hpp index 6fe3b97be..ebf82967f 100644 --- a/src/controllers/plugins/api/HTTPRequest.hpp +++ b/src/controllers/plugins/api/HTTPRequest.hpp @@ -16,11 +16,11 @@ namespace chatterino::lua::api { // NOLINTBEGIN(readability-identifier-naming) /** - * @lua@alias HTTPCallback fun(result: HTTPResponse): nil + * @lua@alias c2.HTTPCallback fun(result: c2.HTTPResponse): nil */ /** - * @lua@class HTTPRequest + * @lua@class c2.HTTPRequest */ class HTTPRequest : public std::enable_shared_from_this { @@ -61,16 +61,16 @@ public: /** * Sets the success callback * - * @lua@param callback HTTPCallback Function to call when the HTTP request succeeds - * @exposed HTTPRequest:on_success + * @lua@param callback c2.HTTPCallback Function to call when the HTTP request succeeds + * @exposed c2.HTTPRequest:on_success */ void on_success(sol::protected_function func); /** * Sets the failure callback * - * @lua@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status - * @exposed HTTPRequest:on_error + * @lua@param callback c2.HTTPCallback Function to call when the HTTP request fails or returns a non-ok status + * @exposed c2.HTTPRequest:on_error */ void on_error(sol::protected_function func); @@ -78,7 +78,7 @@ public: * Sets the finally callback * * @lua@param callback fun(): nil Function to call when the HTTP request finishes - * @exposed HTTPRequest:finally + * @exposed c2.HTTPRequest:finally */ void finally(sol::protected_function func); @@ -86,7 +86,7 @@ public: * Sets the timeout * * @lua@param timeout integer How long in milliseconds until the times out - * @exposed HTTPRequest:set_timeout + * @exposed c2.HTTPRequest:set_timeout */ void set_timeout(int timeout); @@ -94,7 +94,7 @@ public: * Sets the request payload * * @lua@param data string - * @exposed HTTPRequest:set_payload + * @exposed c2.HTTPRequest:set_payload */ void set_payload(QByteArray payload); @@ -103,19 +103,19 @@ public: * * @lua@param name string * @lua@param value string - * @exposed HTTPRequest:set_header + * @exposed c2.HTTPRequest:set_header */ void set_header(QByteArray name, QByteArray value); /** * Executes the HTTP request * - * @exposed HTTPRequest:execute + * @exposed c2.HTTPRequest:execute */ void execute(sol::this_state L); /** * @lua@return string - * @exposed HTTPRequest:__tostring + * @exposed c2.HTTPRequest:__tostring */ QString to_string(); @@ -126,11 +126,11 @@ public: /** * Creates a new HTTPRequest * - * @lua@param method HTTPMethod Method to use + * @lua@param method c2.HTTPMethod Method to use * @lua@param url string Where to send the request to * - * @lua@return HTTPRequest - * @exposed HTTPRequest.create + * @lua@return c2.HTTPRequest + * @exposed c2.HTTPRequest.create */ static std::shared_ptr create(sol::this_state L, NetworkRequestType method, diff --git a/src/controllers/plugins/api/HTTPResponse.hpp b/src/controllers/plugins/api/HTTPResponse.hpp index 80eb49bd3..9997c858d 100644 --- a/src/controllers/plugins/api/HTTPResponse.hpp +++ b/src/controllers/plugins/api/HTTPResponse.hpp @@ -15,7 +15,7 @@ namespace chatterino::lua::api { // NOLINTBEGIN(readability-identifier-naming) /** - * @lua@class HTTPResponse + * @lua@class c2.HTTPResponse */ class HTTPResponse { @@ -38,26 +38,34 @@ public: * Returns the data. This is not guaranteed to be encoded using any * particular encoding scheme. It's just the bytes the server returned. * - * @exposed HTTPResponse:data + * @lua@return string + * @lua@nodiscard + * @exposed c2.HTTPResponse:data */ QByteArray data(); /** * Returns the status code. * - * @exposed HTTPResponse:status + * @lua@return number|nil + * @lua@nodiscard + * @exposed c2.HTTPResponse:status */ std::optional status(); /** * A somewhat human readable description of an error if such happened - * @exposed HTTPResponse:error + * + * @lua@return string + * @lua@nodiscard + * @exposed c2.HTTPResponse:error */ QString error(); /** * @lua@return string - * @exposed HTTPResponse:__tostring + * @lua@nodiscard + * @exposed c2.HTTPResponse:__tostring */ QString to_string(); }; From 5f76f5b755f42744697985f79ffb21aac6541a87 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 2 Nov 2024 13:11:59 +0100 Subject: [PATCH 35/40] fix: take indices to messages as a hint (#5683) --- CHANGELOG.md | 1 + src/common/Channel.cpp | 22 ++++- src/common/Channel.hpp | 11 ++- src/messages/LimitedQueue.hpp | 68 +++++++++++++- src/widgets/helper/ChannelView.cpp | 25 ++--- src/widgets/helper/ChannelView.hpp | 3 +- tests/src/LimitedQueue.cpp | 143 ++++++++++++++++++++++++++++- 7 files changed, 246 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6903bfc72..d22780b17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ - Bugfix: Fixed double-click selection not working when clicking outside a message. (#5617) - Bugfix: Fixed emotes starting with ":" not tab-completing. (#5603) - Bugfix: Fixed 7TV emotes messing with Qt's HTML. (#5677) +- Bugfix: Fixed incorrect messages getting replaced visually. (#5683) - Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420) - Dev: Update vcpkg build Qt from 6.5.0 to 6.7.0, boost from 1.83.0 to 1.85.0, openssl from 3.1.3 to 3.3.0. (#5422) - Dev: Unsingletonize `ISoundController`. (#5462) diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index ef778bad1..ecedf22a1 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -253,21 +253,33 @@ void Channel::fillInMissingMessages(const std::vector &messages) } } -void Channel::replaceMessage(MessagePtr message, MessagePtr replacement) +void Channel::replaceMessage(const MessagePtr &message, + const MessagePtr &replacement) { int index = this->messages_.replaceItem(message, replacement); if (index >= 0) { - this->messageReplaced.invoke((size_t)index, replacement); + this->messageReplaced.invoke((size_t)index, message, replacement); } } -void Channel::replaceMessage(size_t index, MessagePtr replacement) +void Channel::replaceMessage(size_t index, const MessagePtr &replacement) { - if (this->messages_.replaceItem(index, replacement)) + MessagePtr prev; + if (this->messages_.replaceItem(index, replacement, &prev)) { - this->messageReplaced.invoke(index, replacement); + this->messageReplaced.invoke(index, prev, replacement); + } +} + +void Channel::replaceMessage(size_t hint, const MessagePtr &message, + const MessagePtr &replacement) +{ + auto index = this->messages_.replaceItem(hint, message, replacement); + if (index >= 0) + { + this->messageReplaced.invoke(hint, message, replacement); } } diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index ac90573ff..0b71bf0ca 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -66,7 +66,9 @@ public: pajlada::Signals::Signal> messageAppended; pajlada::Signals::Signal &> messagesAddedAtStart; - pajlada::Signals::Signal messageReplaced; + /// (index, prev-message, replacement) + pajlada::Signals::Signal + messageReplaced; /// Invoked when some number of messages were filled in using time received pajlada::Signals::Signal &> filledInMessages; pajlada::Signals::NoArgSignal destroyed; @@ -96,8 +98,11 @@ public: void addOrReplaceTimeout(MessagePtr message); void disableAllMessages(); - void replaceMessage(MessagePtr message, MessagePtr replacement); - void replaceMessage(size_t index, MessagePtr replacement); + void replaceMessage(const MessagePtr &message, + const MessagePtr &replacement); + void replaceMessage(size_t index, const MessagePtr &replacement); + void replaceMessage(size_t hint, const MessagePtr &message, + const MessagePtr &replacement); void deleteMessage(QString messageID); /// Removes all messages from this channel and invokes #messagesCleared diff --git a/src/messages/LimitedQueue.hpp b/src/messages/LimitedQueue.hpp index a204c84a9..4d223ab15 100644 --- a/src/messages/LimitedQueue.hpp +++ b/src/messages/LimitedQueue.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace chatterino { @@ -212,9 +213,10 @@ public: * * @param[in] index the index of the item to replace * @param[in] replacement the item to put in place of the item at index + * @param[out] prev (optional) the item located at @a index before replacing * @return true if a replacement took place */ - bool replaceItem(size_t index, const T &replacement) + bool replaceItem(size_t index, const T &replacement, T *prev = nullptr) { std::unique_lock lock(this->mutex_); @@ -223,10 +225,46 @@ public: return false; } - this->buffer_[index] = replacement; + if (prev) + { + *prev = std::exchange(this->buffer_[index], replacement); + } + else + { + this->buffer_[index] = replacement; + } return true; } + /** + * @brief Replace the needle with the given item + * + * @param hint A hint on where the needle _might_ be + * @param[in] needle the item to search for + * @param[in] replacement the item to replace needle with + * @return the index of the replaced item, or -1 if no replacement took place + */ + int replaceItem(size_t hint, const T &needle, const T &replacement) + { + std::unique_lock lock(this->mutex_); + + if (hint < this->buffer_.size() && this->buffer_[hint] == needle) + { + this->buffer_[hint] = replacement; + return static_cast(hint); + } + + for (size_t i = 0; i < this->buffer_.size(); ++i) + { + if (this->buffer_[i] == needle) + { + this->buffer_[i] = replacement; + return static_cast(i); + } + } + return -1; + } + /** * @brief Inserts the given item before another item * @@ -315,6 +353,32 @@ public: return std::nullopt; } + /** + * @brief Find an item with a hint + * + * @param hint A hint on where the needle _might_ be + * @param predicate that will used to find the item + * @return the item and its index or none if it's not found + */ + std::optional> find(size_t hint, auto &&predicate) + { + std::unique_lock lock(this->mutex_); + + if (hint < this->buffer_.size() && predicate(this->buffer_[hint])) + { + return std::pair{hint, this->buffer_[hint]}; + }; + + for (size_t i = 0; i < this->buffer_.size(); i++) + { + if (predicate(this->buffer_[i])) + { + return std::pair{i, this->buffer_[i]}; + } + } + return std::nullopt; + } + /** * @brief Returns the first item matching a predicate, checking in reverse * diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index dc0a98cbd..a5fa62582 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -967,10 +967,10 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) this->channelConnections_.managedConnect( underlyingChannel->messageReplaced, - [this](auto index, const auto &replacement) { + [this](auto index, const auto &prev, const auto &replacement) { if (this->shouldIncludeMessage(replacement)) { - this->channel_->replaceMessage(index, replacement); + this->channel_->replaceMessage(index, prev, replacement); } }); @@ -1051,8 +1051,9 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) // on message replaced this->channelConnections_.managedConnect( this->channel_->messageReplaced, - [this](size_t index, MessagePtr replacement) { - this->messageReplaced(index, replacement); + [this](size_t index, const MessagePtr &prev, + const MessagePtr &replacement) { + this->messageReplaced(index, prev, replacement); }); // on messages filled in @@ -1258,19 +1259,21 @@ void ChannelView::messageAddedAtStart(std::vector &messages) this->queueLayout(); } -void ChannelView::messageReplaced(size_t index, MessagePtr &replacement) +void ChannelView::messageReplaced(size_t hint, const MessagePtr &prev, + const MessagePtr &replacement) { - auto oMessage = this->messages_.get(index); - if (!oMessage) + auto optItem = this->messages_.find(hint, [&](const auto &it) { + return it->getMessagePtr() == prev; + }); + if (!optItem) { return; } - - auto message = *oMessage; + const auto &[index, oldItem] = *optItem; auto newItem = std::make_shared(replacement); - if (message->flags.has(MessageLayoutFlag::AlternateBackground)) + if (oldItem->flags.has(MessageLayoutFlag::AlternateBackground)) { newItem->flags.set(MessageLayoutFlag::AlternateBackground); } @@ -1278,7 +1281,7 @@ void ChannelView::messageReplaced(size_t index, MessagePtr &replacement) this->scrollBar_->replaceHighlight(index, replacement->getScrollBarHighlight()); - this->messages_.replaceItem(message, newItem); + this->messages_.replaceItem(index, newItem); this->queueLayout(); } diff --git a/src/widgets/helper/ChannelView.hpp b/src/widgets/helper/ChannelView.hpp index 704210712..40b40444e 100644 --- a/src/widgets/helper/ChannelView.hpp +++ b/src/widgets/helper/ChannelView.hpp @@ -268,7 +268,8 @@ private: std::optional overridingFlags); void messageAddedAtStart(std::vector &messages); void messageRemoveFromStart(MessagePtr &message); - void messageReplaced(size_t index, MessagePtr &replacement); + void messageReplaced(size_t hint, const MessagePtr &prev, + const MessagePtr &replacement); void messagesUpdated(); void performLayout(bool causedByScrollbar = false, diff --git a/tests/src/LimitedQueue.cpp b/tests/src/LimitedQueue.cpp index 0a94ea928..803969789 100644 --- a/tests/src/LimitedQueue.cpp +++ b/tests/src/LimitedQueue.cpp @@ -114,20 +114,153 @@ TEST(LimitedQueue, PushFront) TEST(LimitedQueue, ReplaceItem) { - LimitedQueue queue(5); + LimitedQueue queue(10); queue.pushBack(1); queue.pushBack(2); queue.pushBack(3); + queue.pushBack(4); + queue.pushBack(5); + queue.pushBack(6); int idex = queue.replaceItem(2, 10); EXPECT_EQ(idex, 1); - idex = queue.replaceItem(5, 11); + idex = queue.replaceItem(7, 11); EXPECT_EQ(idex, -1); - bool res = queue.replaceItem(std::size_t(0), 9); + int prev = -1; + bool res = queue.replaceItem(std::size_t(0), 9, &prev); EXPECT_TRUE(res); - res = queue.replaceItem(std::size_t(5), 4); + EXPECT_EQ(prev, 1); + res = queue.replaceItem(std::size_t(6), 4); EXPECT_FALSE(res); - SNAPSHOT_EQUALS(queue.getSnapshot(), {9, 10, 3}, "first snapshot"); + // correct hint + EXPECT_EQ(queue.replaceItem(3, 4, 11), 3); + // incorrect hints + EXPECT_EQ(queue.replaceItem(5, 11, 12), 3); + EXPECT_EQ(queue.replaceItem(0, 12, 13), 3); + // oob hint + EXPECT_EQ(queue.replaceItem(42, 13, 14), 3); + // bad needle + EXPECT_EQ(queue.replaceItem(0, 15, 16), -1); + + SNAPSHOT_EQUALS(queue.getSnapshot(), {9, 10, 3, 14, 5, 6}, + "first snapshot"); +} + +TEST(LimitedQueue, Find) +{ + LimitedQueue queue(10); + queue.pushBack(1); + queue.pushBack(2); + queue.pushBack(3); + queue.pushBack(4); + queue.pushBack(5); + queue.pushBack(6); + + // without hint + EXPECT_FALSE(queue + .find([](int i) { + return i == 0; + }) + .has_value()); + EXPECT_EQ(queue + .find([](int i) { + return i == 1; + }) + .value(), + 1); + EXPECT_EQ(queue + .find([](int i) { + return i == 2; + }) + .value(), + 2); + EXPECT_EQ(queue + .find([](int i) { + return i == 6; + }) + .value(), + 6); + EXPECT_FALSE(queue + .find([](int i) { + return i == 7; + }) + .has_value()); + EXPECT_FALSE(queue + .find([](int i) { + return i > 6; + }) + .has_value()); + EXPECT_FALSE(queue + .find([](int i) { + return i <= 0; + }) + .has_value()); + + using Pair = std::pair; + // with hint + EXPECT_FALSE(queue + .find(0, + [](int i) { + return i == 0; + }) + .has_value()); + // correct hint + EXPECT_EQ(queue + .find(0, + [](int i) { + return i == 1; + }) + .value(), + (Pair{0, 1})); + EXPECT_EQ(queue + .find(1, + [](int i) { + return i == 2; + }) + .value(), + (Pair{1, 2})); + // incorrect hint + EXPECT_EQ(queue + .find(1, + [](int i) { + return i == 1; + }) + .value(), + (Pair{0, 1})); + EXPECT_EQ(queue + .find(5, + [](int i) { + return i == 6; + }) + .value(), + (Pair{5, 6})); + // oob hint + EXPECT_EQ(queue + .find(6, + [](int i) { + return i == 3; + }) + .value(), + (Pair{2, 3})); + // non-existent items + EXPECT_FALSE(queue + .find(42, + [](int i) { + return i == 7; + }) + .has_value()); + EXPECT_FALSE(queue + .find(0, + [](int i) { + return i > 6; + }) + .has_value()); + EXPECT_FALSE(queue + .find(0, + [](int i) { + return i <= 0; + }) + .has_value()); } From 101a45fd3a43b5954961ad78f45b26df4bb34b0f Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 2 Nov 2024 13:54:31 +0100 Subject: [PATCH 36/40] refactor: deduplicate IRC parsing (#5678) --- CHANGELOG.md | 1 + src/CMakeLists.txt | 6 + src/common/Channel.cpp | 26 +- src/common/Channel.hpp | 31 +- src/common/enums/MessageContext.hpp | 13 + src/messages/MessageSimilarity.cpp | 121 +++ src/messages/MessageSimilarity.hpp | 11 + src/messages/MessageSink.hpp | 67 ++ src/providers/recentmessages/Impl.cpp | 24 +- src/providers/twitch/IrcMessageHandler.cpp | 856 ++++-------------- src/providers/twitch/IrcMessageHandler.hpp | 19 +- src/providers/twitch/TwitchChannel.cpp | 6 +- src/providers/twitch/TwitchIrcServer.cpp | 2 +- src/util/VectorMessageSink.cpp | 86 ++ src/util/VectorMessageSink.hpp | 36 + .../IrcMessageHandler/announcement.json | 1 + .../IrcMessageHandler/reply-child.json | 3 +- .../shared-chat-announcement.json | 1 + .../IrcMessageHandler/sub-message.json | 1 + tests/src/IrcMessageHandler.cpp | 18 +- 20 files changed, 590 insertions(+), 739 deletions(-) create mode 100644 src/common/enums/MessageContext.hpp create mode 100644 src/messages/MessageSimilarity.cpp create mode 100644 src/messages/MessageSimilarity.hpp create mode 100644 src/messages/MessageSink.hpp create mode 100644 src/util/VectorMessageSink.cpp create mode 100644 src/util/VectorMessageSink.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index d22780b17..dc3924f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,7 @@ - Dev: Decoupled reply parsing from `MessageBuilder`. (#5660, #5668) - Dev: Refactored IRC message building. (#5663) - Dev: Fixed some compiler warnings. (#5672) +- Dev: Unified parsing of historic and live IRC messages. (#5678) ## 2.5.1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bace57a9c..631239533 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,6 +40,7 @@ set(SOURCE_FILES common/WindowDescriptors.cpp common/WindowDescriptors.hpp + common/enums/MessageContext.hpp common/enums/MessageOverflow.hpp common/network/NetworkCommon.cpp @@ -282,6 +283,9 @@ set(SOURCE_FILES messages/MessageElement.cpp messages/MessageElement.hpp messages/MessageFlag.hpp + messages/MessageSimilarity.cpp + messages/MessageSimilarity.hpp + messages/MessageSink.hpp messages/MessageThread.cpp messages/MessageThread.hpp @@ -527,6 +531,8 @@ set(SOURCE_FILES util/Twitch.hpp util/TypeName.hpp util/Variant.hpp + util/VectorMessageSink.cpp + util/VectorMessageSink.hpp util/WidgetHelpers.cpp util/WidgetHelpers.hpp util/WindowsHelper.cpp diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index ecedf22a1..6396ef00c 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -3,7 +3,9 @@ #include "Application.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" +#include "messages/MessageSimilarity.hpp" #include "providers/twitch/IrcMessageHandler.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Emotes.hpp" #include "singletons/Logging.hpp" #include "singletons/Settings.hpp" @@ -121,10 +123,10 @@ void Channel::addSystemMessage(const QString &contents) this->addMessage(msg, MessageContext::Original); } -void Channel::addOrReplaceTimeout(MessagePtr message) +void Channel::addOrReplaceTimeout(MessagePtr message, QTime now) { addOrReplaceChannelTimeout( - this->getMessageSnapshot(), std::move(message), QTime::currentTime(), + this->getMessageSnapshot(), std::move(message), now, [this](auto /*idx*/, auto msg, auto replacement) { this->replaceMessage(msg, replacement); }, @@ -299,10 +301,15 @@ void Channel::clearMessages() } MessagePtr Channel::findMessage(QString messageID) +{ + return this->findMessageByID(messageID); +} + +MessagePtr Channel::findMessageByID(QStringView messageID) { MessagePtr res; - if (auto msg = this->messages_.rfind([&messageID](const MessagePtr &msg) { + if (auto msg = this->messages_.rfind([messageID](const MessagePtr &msg) { return msg->id == messageID; }); msg) @@ -313,6 +320,19 @@ MessagePtr Channel::findMessage(QString messageID) return res; } +void Channel::applySimilarityFilters(const MessagePtr &message) const +{ + setSimilarityFlags(message, this->messages_.getSnapshot()); +} + +MessageSinkTraits Channel::sinkTraits() const +{ + return { + MessageSinkTrait::AddMentionsToGlobalChannel, + MessageSinkTrait::RequiresKnownChannelPointReward, + }; +} + bool Channel::canSendMessage() const { return false; diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 0b71bf0ca..c7d006f1b 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -1,8 +1,10 @@ #pragma once +#include "common/enums/MessageContext.hpp" #include "controllers/completion/TabCompletionModel.hpp" #include "messages/LimitedQueue.hpp" #include "messages/MessageFlag.hpp" +#include "messages/MessageSink.hpp" #include #include @@ -26,15 +28,7 @@ enum class TimeoutStackStyle : int { Default = DontStackBeyondUserMessage, }; -/// Context of the message being added to a channel -enum class MessageContext { - /// This message is the original - Original, - /// This message is a repost of a message that has already been added in a channel - Repost, -}; - -class Channel : public std::enable_shared_from_this +class Channel : public std::enable_shared_from_this, public MessageSink { public: // This is for Lua. See scripts/make_luals_meta.py @@ -55,7 +49,7 @@ public: }; explicit Channel(const QString &name, Type type); - virtual ~Channel(); + ~Channel() override; // SIGNALS pajlada::Signals::Signal @@ -87,8 +81,9 @@ public: // overridingFlags can be filled in with flags that should be used instead // of the message's flags. This is useful in case a flag is specific to a // type of split - void addMessage(MessagePtr message, MessageContext context, - std::optional overridingFlags = std::nullopt); + void addMessage( + MessagePtr message, MessageContext context, + std::optional overridingFlags = std::nullopt) final; void addMessagesAtStart(const std::vector &messages_); void addSystemMessage(const QString &contents); @@ -96,8 +91,8 @@ public: /// Inserts the given messages in order by Message::serverReceivedTime. void fillInMissingMessages(const std::vector &messages); - void addOrReplaceTimeout(MessagePtr message); - void disableAllMessages(); + void addOrReplaceTimeout(MessagePtr message, QTime now) final; + void disableAllMessages() final; void replaceMessage(const MessagePtr &message, const MessagePtr &replacement); void replaceMessage(size_t index, const MessagePtr &replacement); @@ -108,10 +103,16 @@ public: /// Removes all messages from this channel and invokes #messagesCleared void clearMessages(); - MessagePtr findMessage(QString messageID); + [[deprecated("Use findMessageByID instead")]] MessagePtr findMessage( + QString messageID); + MessagePtr findMessageByID(QStringView messageID) final; bool hasMessages() const; + void applySimilarityFilters(const MessagePtr &message) const final; + + MessageSinkTraits sinkTraits() const final; + // CHANNEL INFO virtual bool canSendMessage() const; virtual bool isWritable() const; // whether split input will be usable diff --git a/src/common/enums/MessageContext.hpp b/src/common/enums/MessageContext.hpp new file mode 100644 index 000000000..669e55315 --- /dev/null +++ b/src/common/enums/MessageContext.hpp @@ -0,0 +1,13 @@ +#pragma once + +namespace chatterino { + +/// Context of the message being added to a channel +enum class MessageContext { + /// This message is the original + Original, + /// This message is a repost of a message that has already been added in a channel + Repost, +}; + +} // namespace chatterino diff --git a/src/messages/MessageSimilarity.cpp b/src/messages/MessageSimilarity.cpp new file mode 100644 index 000000000..2f8157d6b --- /dev/null +++ b/src/messages/MessageSimilarity.cpp @@ -0,0 +1,121 @@ +#include "messages/MessageSimilarity.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "messages/LimitedQueueSnapshot.hpp" // IWYU pragma: keep +#include "providers/twitch/TwitchAccount.hpp" +#include "singletons/Settings.hpp" + +#include +#include + +namespace { + +using namespace chatterino; + +float relativeSimilarity(QStringView str1, QStringView str2) +{ + using SizeType = QStringView::size_type; + + // Longest Common Substring Problem + std::vector> tree(str1.size(), + std::vector(str2.size(), 0)); + int z = 0; + + for (SizeType i = 0; i < str1.size(); ++i) + { + for (SizeType j = 0; j < str2.size(); ++j) + { + if (str1[i] == str2[j]) + { + if (i == 0 || j == 0) + { + tree[i][j] = 1; + } + else + { + tree[i][j] = tree[i - 1][j - 1] + 1; + } + z = std::max(tree[i][j], z); + } + else + { + tree[i][j] = 0; + } + } + } + + // ensure that no div by 0 + if (z == 0) + { + return 0.F; + } + + auto div = std::max<>({static_cast(1), str1.size(), str2.size()}); + + return float(z) / float(div); +} + +template +float inMessages(const MessagePtr &msg, const T &messages) +{ + float similarityPercent = 0.0F; + + for (const auto &prevMsg : + messages | std::views::reverse | + std::views::take(getSettings()->hideSimilarMaxMessagesToCheck)) + { + if (prevMsg->parseTime.secsTo(QTime::currentTime()) >= + getSettings()->hideSimilarMaxDelay) + { + break; + } + if (getSettings()->hideSimilarBySameUser && + msg->loginName != prevMsg->loginName) + { + continue; + } + similarityPercent = std::max( + similarityPercent, + relativeSimilarity(msg->messageText, prevMsg->messageText)); + } + + return similarityPercent; +} + +} // namespace + +namespace chatterino { + +template +void setSimilarityFlags(const MessagePtr &message, const T &messages) +{ + if (getSettings()->similarityEnabled) + { + bool isMyself = + message->loginName == + getApp()->getAccounts()->twitch.getCurrent()->getUserName(); + bool hideMyself = getSettings()->hideSimilarMyself; + + if (isMyself && !hideMyself) + { + return; + } + + if (inMessages(message, messages) > getSettings()->similarityPercentage) + { + message->flags.set(MessageFlag::Similar); + if (getSettings()->colorSimilarDisabled) + { + message->flags.set(MessageFlag::Disabled); + } + } + } +} + +template void setSimilarityFlags>( + const MessagePtr &msg, const std::vector &messages); +template void setSimilarityFlags>( + const MessagePtr &msg, const LimitedQueueSnapshot &messages); + +} // namespace chatterino diff --git a/src/messages/MessageSimilarity.hpp b/src/messages/MessageSimilarity.hpp new file mode 100644 index 000000000..54d0214d7 --- /dev/null +++ b/src/messages/MessageSimilarity.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "messages/Message.hpp" + +#include +namespace chatterino { + +template +void setSimilarityFlags(const MessagePtr &message, const T &messages); + +} // namespace chatterino diff --git a/src/messages/MessageSink.hpp b/src/messages/MessageSink.hpp new file mode 100644 index 000000000..e720a1867 --- /dev/null +++ b/src/messages/MessageSink.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "common/enums/MessageContext.hpp" +#include "common/FlagsEnum.hpp" +#include "messages/MessageFlag.hpp" + +#include +#include + +class QStringView; +class QTime; + +namespace chatterino { + +struct Message; +using MessagePtr = std::shared_ptr; + +enum class MessageSinkTrait : uint8_t { + None = 0, + + /// Messages with the `Highlighted` and `ShowInMentions` flags should be + /// added to the global mentions channel when encountered. + AddMentionsToGlobalChannel = 1 << 0, + + /// A channel-point redemption whose reward is not yet known should not be + /// added to this sink, but queued in the corresponding TwitchChannel + /// (`addQueuedRedemption`). + RequiresKnownChannelPointReward = 1 << 1, +}; +using MessageSinkTraits = FlagsEnum; + +/// A generic interface for a managed buffer of `Message`s +class MessageSink +{ +public: + virtual ~MessageSink() = default; + + /// Add a message to this sink + /// + /// @param message The message to add (non-null) + /// @param ctx The context in which this message is being added. + /// @param overridingFlags + virtual void addMessage( + MessagePtr message, MessageContext ctx, + std::optional overridingFlags = std::nullopt) = 0; + + /// Adds a timeout message or merges it into an existing one + virtual void addOrReplaceTimeout(MessagePtr clearchatMessage, + QTime now) = 0; + + /// Flags all messages as `Disabled` + virtual void disableAllMessages() = 0; + + /// Searches for similar messages and flags this message as similar + /// (based on the current settings). + virtual void applySimilarityFilters(const MessagePtr &message) const = 0; + + /// @brief Searches for a message by an ID + /// + /// If there is no message found, an empty shared-pointer is returned. + virtual MessagePtr findMessageByID(QStringView id) = 0; + + /// Behaviour to be exercised when parsing/building messages for this sink. + virtual MessageSinkTraits sinkTraits() const = 0; +}; + +} // namespace chatterino diff --git a/src/providers/recentmessages/Impl.cpp b/src/providers/recentmessages/Impl.cpp index 410a34aac..4605204eb 100644 --- a/src/providers/recentmessages/Impl.cpp +++ b/src/providers/recentmessages/Impl.cpp @@ -3,7 +3,9 @@ #include "common/Env.hpp" #include "messages/MessageBuilder.hpp" #include "providers/twitch/IrcMessageHandler.hpp" +#include "providers/twitch/TwitchChannel.hpp" #include "util/Helpers.hpp" +#include "util/VectorMessageSink.hpp" #include #include @@ -40,7 +42,13 @@ std::vector parseRecentMessages( std::vector buildRecentMessages( std::vector &messages, Channel *channel) { - std::vector allBuiltMessages; + VectorMessageSink sink({}, MessageFlag::RecentMessage); + + auto *twitchChannel = dynamic_cast(channel); + if (!twitchChannel) + { + return {}; + } for (auto *message : messages) { @@ -58,24 +66,16 @@ std::vector buildRecentMessages( auto msg = makeSystemMessage( QLocale().toString(msgDate, QLocale::LongFormat), QTime(0, 0)); - msg->flags.set(MessageFlag::RecentMessage); - allBuiltMessages.emplace_back(msg); + sink.addMessage(msg, MessageContext::Original); } } - auto builtMessages = IrcMessageHandler::parseMessageWithReply( - channel, message, allBuiltMessages); - - for (const auto &builtMessage : builtMessages) - { - builtMessage->flags.set(MessageFlag::RecentMessage); - allBuiltMessages.emplace_back(builtMessage); - } + IrcMessageHandler::parseMessageInto(message, sink, twitchChannel); message->deleteLater(); } - return allBuiltMessages; + return std::move(sink).takeMessages(); } // Returns the URL to be used for querying the Recent Messages API for the diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 249bd6c3c..7b05fea48 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -7,24 +7,21 @@ #include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/ignores/IgnoreController.hpp" -#include "messages/LimitedQueue.hpp" #include "messages/Link.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "messages/MessageColor.hpp" #include "messages/MessageElement.hpp" +#include "messages/MessageSink.hpp" #include "messages/MessageThread.hpp" -#include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchAccountManager.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchHelpers.hpp" #include "providers/twitch/TwitchIrcServer.hpp" -#include "singletons/Resources.hpp" #include "singletons/Settings.hpp" #include "singletons/StreamerMode.hpp" #include "singletons/WindowManager.hpp" -#include "util/ChannelHelpers.hpp" #include "util/FormatTime.hpp" #include "util/Helpers.hpp" #include "util/IrcHelpers.hpp" @@ -34,7 +31,6 @@ #include #include -#include using namespace chatterino::literals; @@ -165,50 +161,6 @@ ChannelPtr channelOrEmptyByTarget(const QString &target, return server.getChannelOrEmpty(channelName); } -float relativeSimilarity(const QString &str1, const QString &str2) -{ - // Longest Common Substring Problem - std::vector> tree(str1.size(), - std::vector(str2.size(), 0)); - int z = 0; - - for (int i = 0; i < str1.size(); ++i) - { - for (int j = 0; j < str2.size(); ++j) - { - if (str1[i] == str2[j]) - { - if (i == 0 || j == 0) - { - tree[i][j] = 1; - } - else - { - tree[i][j] = tree[i - 1][j - 1] + 1; - } - if (tree[i][j] > z) - { - z = tree[i][j]; - } - } - else - { - tree[i][j] = 0; - } - } - } - - // ensure that no div by 0 - if (z == 0) - { - return 0.F; - } - - auto div = std::max(1, std::max(str1.size(), str2.size())); - - return float(z) / float(div); -} - QMap parseBadges(const QString &badgesString) { QMap badges; @@ -232,106 +184,6 @@ struct ReplyContext { MessagePtr parent; }; -[[nodiscard]] ReplyContext getReplyContext( - TwitchChannel *channel, Communi::IrcMessage *message, - const std::vector &otherLoaded) -{ - ReplyContext ctx; - - const auto &tags = message->tags(); - if (const auto it = tags.find("reply-thread-parent-msg-id"); - it != tags.end()) - { - const QString replyID = it.value().toString(); - auto threadIt = channel->threads().find(replyID); - std::shared_ptr rootThread; - if (threadIt != channel->threads().end()) - { - auto owned = threadIt->second.lock(); - if (owned) - { - // Thread already exists (has a reply) - checkThreadSubscription(tags, message->nick(), owned); - ctx.thread = owned; - rootThread = owned; - } - } - - if (!rootThread) - { - MessagePtr foundMessage; - - // Thread does not yet exist, find root reply and create thread. - // Linear search is justified by the infrequent use of replies - for (const auto &otherMsg : otherLoaded) - { - if (otherMsg->id == replyID) - { - // Found root reply message - foundMessage = otherMsg; - break; - } - } - - if (!foundMessage) - { - // We didn't find the reply root message in the otherLoaded messages - // which are typically the already-parsed recent messages from the - // Recent Messages API. We could have a really old message that - // still exists being replied to, so check for that here. - foundMessage = channel->findMessage(replyID); - } - - if (foundMessage) - { - std::shared_ptr newThread = - std::make_shared(foundMessage); - checkThreadSubscription(tags, message->nick(), newThread); - - ctx.thread = newThread; - rootThread = newThread; - // Store weak reference to thread in channel - channel->addReplyThread(newThread); - } - } - - if (const auto parentIt = tags.find("reply-parent-msg-id"); - parentIt != tags.end()) - { - const QString parentID = parentIt.value().toString(); - if (replyID == parentID) - { - if (rootThread) - { - ctx.parent = rootThread->root(); - } - } - else - { - auto parentThreadIt = channel->threads().find(parentID); - if (parentThreadIt != channel->threads().end()) - { - auto thread = parentThreadIt->second.lock(); - if (thread) - { - ctx.parent = thread->root(); - } - } - else - { - auto parent = channel->findMessage(parentID); - if (parent) - { - ctx.parent = parent; - } - } - } - } - } - - return ctx; -} - std::optional parseClearChatMessage( Communi::IrcMessage *message) { @@ -370,9 +222,9 @@ std::optional parseClearChatMessage( } /** - * Parse a single IRC NOTICE message into 0 or more Chatterino messages + * Parse a single IRC NOTICE message into a Chatterino message **/ -std::vector parseNoticeMessage(Communi::IrcNoticeMessage *message) +MessagePtr parseNoticeMessage(Communi::IrcNoticeMessage *message) { assert(message != nullptr); @@ -400,7 +252,7 @@ std::vector parseNoticeMessage(Communi::IrcNoticeMessage *message) linkColor) ->setLink(accountsLink); - return {builder.release()}; + return builder.release(); } if (message->content().startsWith("You are permanently banned ")) @@ -410,256 +262,19 @@ std::vector parseNoticeMessage(Communi::IrcNoticeMessage *message) if (message->tags().value("msg-id") == "msg_timedout") { - std::vector builtMessage; - QString remainingTime = formatTime(message->content().split(" ").value(5)); QString formattedMessage = QString("You are timed out for %1.") .arg(remainingTime.isEmpty() ? "0s" : remainingTime); - builtMessage.emplace_back(makeSystemMessage( - formattedMessage, calculateMessageTime(message).time())); - - return builtMessage; + return makeSystemMessage(formattedMessage, + calculateMessageTime(message).time()); } // default case - std::vector builtMessages; - - builtMessages.emplace_back(makeSystemMessage( - message->content(), calculateMessageTime(message).time())); - - return builtMessages; -} - -/** - * Parse a single IRC USERNOTICE message into 0 or more Chatterino messages - **/ -std::vector parseUserNoticeMessage(Channel *channel, - Communi::IrcMessage *message) -{ - assert(channel != nullptr); - assert(message != nullptr); - - std::vector builtMessages; - - auto tags = message->tags(); - auto parameters = message->parameters(); - - QString msgType = tags.value("msg-id").toString(); - bool mirrored = msgType == "sharedchatnotice"; - if (mirrored) - { - msgType = tags.value("source-msg-id").toString(); - } - else - { - auto rIt = tags.find("room-id"); - auto sIt = tags.find("source-room-id"); - if (rIt != tags.end() && sIt != tags.end()) - { - mirrored = rIt.value().toString() != sIt.value().toString(); - } - } - - if (mirrored && msgType != "announcement") - { - // avoid confusing broadcasters with user payments to other channels - return {}; - } - - QString content; - if (parameters.size() >= 2) - { - content = parameters[1]; - } - - if (isIgnoredMessage({ - .message = content, - .twitchUserID = tags.value("user-id").toString(), - .isMod = channel->isMod(), - .isBroadcaster = channel->isBroadcaster(), - })) - { - return {}; - } - - if (SPECIAL_MESSAGE_TYPES.contains(msgType)) - { - // Messages are not required, so they might be empty - if (!content.isEmpty()) - { - MessageParseArgs args; - args.trimSubscriberUsername = true; - args.allowIgnore = false; - - auto [built, highlight] = MessageBuilder::makeIrcMessage( - channel, message, args, content, 0); - if (built) - { - built->flags.set(MessageFlag::Subscription); - built->flags.unset(MessageFlag::Highlighted); - if (mirrored) - { - built->flags.set(MessageFlag::SharedMessage); - } - builtMessages.emplace_back(std::move(built)); - } - } - } - - auto it = tags.find("system-msg"); - - if (it != tags.end()) - { - // By default, we return value of system-msg tag - QString messageText = it.value().toString(); - - if (msgType == "bitsbadgetier") - { - messageText = - QString("%1 just earned a new %2 Bits badge!") - .arg(tags.value("display-name").toString(), - kFormatNumbers( - tags.value("msg-param-threshold").toInt())); - } - else if (msgType == "announcement") - { - messageText = "Announcement"; - } - else if (msgType == "raid") - { - auto login = tags.value("login").toString(); - auto displayName = tags.value("msg-param-displayName").toString(); - - if (!login.isEmpty() && !displayName.isEmpty()) - { - MessageColor color = MessageColor::System; - if (auto colorTag = tags.value("color").value(); - colorTag.isValid()) - { - color = MessageColor(colorTag); - } - - auto b = MessageBuilder( - raidEntryMessage, parseTagString(messageText), login, - displayName, color, calculateMessageTime(message).time()); - - b->flags.set(MessageFlag::Subscription); - if (mirrored) - { - b->flags.set(MessageFlag::SharedMessage); - } - - auto newMessage = b.release(); - builtMessages.emplace_back(newMessage); - return builtMessages; - } - } - else if (msgType == "subgift") - { - if (auto monthsIt = tags.find("msg-param-gift-months"); - monthsIt != tags.end()) - { - int months = monthsIt.value().toInt(); - if (months > 1) - { - auto plan = tags.value("msg-param-sub-plan").toString(); - QString name = - ANONYMOUS_GIFTER_ID == tags.value("user-id").toString() - ? "An anonymous user" - : tags.value("display-name").toString(); - messageText = - QString("%1 gifted %2 months of a Tier %3 sub to %4!") - .arg(name, QString::number(months), - plan.isEmpty() ? '1' : plan.at(0), - tags.value("msg-param-recipient-display-name") - .toString()); - - if (auto countIt = tags.find("msg-param-sender-count"); - countIt != tags.end()) - { - int count = countIt.value().toInt(); - if (count > months) - { - messageText += - QString( - " They've gifted %1 months in the channel.") - .arg(QString::number(count)); - } - } - } - } - } - else if (msgType == "sub" || msgType == "resub") - { - if (auto tenure = tags.find("msg-param-multimonth-tenure"); - tenure != tags.end() && tenure.value().toInt() == 0) - { - int months = - tags.value("msg-param-multimonth-duration").toInt(); - if (months > 1) - { - int tier = tags.value("msg-param-sub-plan").toInt() / 1000; - messageText = - QString( - "%1 subscribed at Tier %2 for %3 months in advance") - .arg(tags.value("display-name").toString(), - QString::number(tier), - QString::number(months)); - if (msgType == "resub") - { - int cumulative = - tags.value("msg-param-cumulative-months").toInt(); - messageText += - QString(", reaching %1 months cumulatively so far!") - .arg(QString::number(cumulative)); - } - else - { - messageText += "!"; - } - } - } - } - - auto b = MessageBuilder(systemMessage, parseTagString(messageText), - calculateMessageTime(message).time()); - b->flags.set(MessageFlag::Subscription); - if (mirrored) - { - b->flags.set(MessageFlag::SharedMessage); - } - - auto newMessage = b.release(); - builtMessages.emplace_back(newMessage); - } - - return builtMessages; -} - -/** - * Parse a single IRC PRIVMSG into 0-1 Chatterino messages - */ -std::vector parsePrivMessage(Channel *channel, - Communi::IrcPrivateMessage *message) -{ - assert(channel != nullptr); - assert(message != nullptr); - - std::vector builtMessages; - MessageParseArgs args; - args.isAction = message->isAction(); - auto [built, alert] = MessageBuilder::makeIrcMessage(channel, message, args, - message->content(), 0); - if (built) - { - builtMessages.emplace_back(std::move(built)); - MessageBuilder::triggerHighlights(channel, alert); - } - - return builtMessages; + return makeSystemMessage(message->content(), + calculateMessageTime(message).time()); } } // namespace @@ -674,65 +289,27 @@ IrcMessageHandler &IrcMessageHandler::instance() return instance; } -std::vector IrcMessageHandler::parseMessageWithReply( - Channel *channel, Communi::IrcMessage *message, - std::vector &otherLoaded) +void IrcMessageHandler::parseMessageInto(Communi::IrcMessage *message, + MessageSink &sink, + TwitchChannel *channel) { - std::vector builtMessages; - auto command = message->command(); if (command == u"PRIVMSG"_s) { - auto *privMsg = dynamic_cast(message); - auto *tc = dynamic_cast(channel); - if (!tc) - { - return parsePrivMessage(channel, privMsg); - } - - QString content = privMsg->content(); - int messageOffset = stripLeadingReplyMention(privMsg->tags(), content); - MessageParseArgs args; - auto tags = privMsg->tags(); - if (const auto it = tags.find("custom-reward-id"); it != tags.end()) - { - args.channelPointRewardId = it.value().toString(); - } - args.isAction = privMsg->isAction(); - - auto replyCtx = getReplyContext(tc, message, otherLoaded); - auto [built, alert] = MessageBuilder::makeIrcMessage( - channel, message, args, content, messageOffset, replyCtx.thread, - replyCtx.parent); - - if (built) - { - builtMessages.emplace_back(built); - MessageBuilder::triggerHighlights(channel, alert); - } - - if (message->tags().contains(u"pinned-chat-paid-amount"_s)) - { - auto ptr = MessageBuilder::buildHypeChatMessage(privMsg); - if (ptr) - { - builtMessages.emplace_back(std::move(ptr)); - } - } - - return builtMessages; + parsePrivMessageInto( + dynamic_cast(message), sink, channel); } - - if (command == u"USERNOTICE"_s) + else if (command == u"USERNOTICE"_s) { - return parseUserNoticeMessage(channel, message); + parseUserNoticeMessageInto(message, sink, channel); } if (command == u"NOTICE"_s) { - return parseNoticeMessage( - dynamic_cast(message)); + sink.addMessage(parseNoticeMessage( + dynamic_cast(message)), + MessageContext::Original); } if (command == u"CLEARCHAT"_s) @@ -740,32 +317,20 @@ std::vector IrcMessageHandler::parseMessageWithReply( auto cc = parseClearChatMessage(message); if (!cc) { - return builtMessages; + return; } auto &clearChat = *cc; if (clearChat.disableAllMessages) { - builtMessages.emplace_back(std::move(clearChat.message)); + sink.addMessage(std::move(clearChat.message), + MessageContext::Original); } else { - addOrReplaceChannelTimeout( - otherLoaded, std::move(clearChat.message), - calculateMessageTime(message).time(), - [&](auto idx, auto /*msg*/, auto &&replacement) { - replacement->flags.set(MessageFlag::RecentMessage); - otherLoaded[idx] = replacement; - }, - [&](auto &&msg) { - builtMessages.emplace_back(msg); - }, - false); + sink.addOrReplaceTimeout(std::move(clearChat.message), + calculateMessageTime(message).time()); } - - return builtMessages; } - - return builtMessages; } void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, @@ -778,32 +343,41 @@ void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, } auto *twitchChannel = dynamic_cast(chan.get()); - - if (twitchChannel != nullptr) + if (!twitchChannel) { - auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); - if (message->tag("user-id") == currentUser->getUserId()) + return; + } + + parsePrivMessageInto(message, *twitchChannel, twitchChannel); +} + +void IrcMessageHandler::parsePrivMessageInto( + Communi::IrcPrivateMessage *message, MessageSink &sink, + TwitchChannel *channel) +{ + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (message->tag("user-id") == currentUser->getUserId()) + { + auto badgesTag = message->tag("badges"); + if (badgesTag.isValid()) { - auto badgesTag = message->tag("badges"); - if (badgesTag.isValid()) - { - auto parsedBadges = parseBadges(badgesTag.toString()); - twitchChannel->setMod(parsedBadges.contains("moderator")); - twitchChannel->setVIP(parsedBadges.contains("vip")); - twitchChannel->setStaff(parsedBadges.contains("staff")); - } + auto parsedBadges = parseBadges(badgesTag.toString()); + channel->setMod(parsedBadges.contains("moderator")); + channel->setVIP(parsedBadges.contains("vip")); + channel->setStaff(parsedBadges.contains("staff")); } } - this->addMessage(message, chan, unescapeZeroWidthJoiner(message->content()), - twitchServer, false, message->isAction()); + IrcMessageHandler::addMessage( + message, sink, channel, unescapeZeroWidthJoiner(message->content()), + *getApp()->getTwitch(), false, message->isAction()); if (message->tags().contains(u"pinned-chat-paid-amount"_s)) { auto ptr = MessageBuilder::buildHypeChatMessage(message); if (ptr) { - chan->addMessage(ptr, MessageContext::Original); + sink.addMessage(ptr, MessageContext::Original); } } } @@ -900,7 +474,8 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message) return; } - chan->addOrReplaceTimeout(std::move(clearChat.message)); + chan->addOrReplaceTimeout(std::move(clearChat.message), + calculateMessageTime(message).time()); // refresh all getApp()->getWindows()->repaintVisibleChatWidgets(chan.get()); @@ -1039,11 +614,24 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *ircMessage) void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, ITwitchIrcServer &twitchServer) +{ + auto target = message->parameter(0); + auto *channel = dynamic_cast( + twitchServer.getChannelOrEmpty(target).get()); + if (!channel) + { + return; + } + parseUserNoticeMessageInto(message, *channel, channel); +} + +void IrcMessageHandler::parseUserNoticeMessageInto(Communi::IrcMessage *message, + MessageSink &sink, + TwitchChannel *channel) { auto tags = message->tags(); auto parameters = message->parameters(); - auto target = parameters[0]; QString msgType = tags.value("msg-id").toString(); bool mirrored = msgType == "sharedchatnotice"; if (mirrored) @@ -1072,12 +660,11 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, content = parameters[1]; } - auto chn = twitchServer.getChannelOrEmpty(target); if (isIgnoredMessage({ .message = content, .twitchUserID = tags.value("user-id").toString(), - .isMod = chn->isMod(), - .isBroadcaster = chn->isBroadcaster(), + .isMod = channel->isMod(), + .isBroadcaster = channel->isBroadcaster(), })) { return; @@ -1088,7 +675,8 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, // Messages are not required, so they might be empty if (!content.isEmpty()) { - this->addMessage(message, chn, content, twitchServer, true, false); + addMessage(message, sink, channel, content, *getApp()->getTwitch(), + true, false); } } @@ -1136,25 +724,7 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, } auto newMessage = b.release(); - QString channelName; - - if (message->parameters().size() < 1) - { - return; - } - - if (!trimChannelName(message->parameter(0), channelName)) - { - return; - } - - auto chan = twitchServer.getChannelOrEmpty(channelName); - - if (!chan->isEmpty()) - { - chan->addMessage(newMessage, MessageContext::Original); - } - + sink.addMessage(newMessage, MessageContext::Original); return; } } @@ -1235,124 +805,104 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, } auto newMessage = b.release(); - QString channelName; - - if (message->parameters().size() < 1) - { - return; - } - - if (!trimChannelName(message->parameter(0), channelName)) - { - return; - } - - auto chan = twitchServer.getChannelOrEmpty(channelName); - - if (!chan->isEmpty()) - { - chan->addMessage(newMessage, MessageContext::Original); - } + sink.addMessage(newMessage, MessageContext::Original); } } void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) { - auto builtMessages = parseNoticeMessage(message); + auto msg = parseNoticeMessage(message); - for (const auto &msg : builtMessages) + QString channelName; + if (!trimChannelName(message->target(), channelName) || + channelName == "jtv") { - QString channelName; - if (!trimChannelName(message->target(), channelName) || - channelName == "jtv") - { - // Notice wasn't targeted at a single channel, send to all twitch - // channels - getApp()->getTwitch()->forEachChannelAndSpecialChannels( - [msg](const auto &c) { - c->addMessage(msg, MessageContext::Original); - }); + // Notice wasn't targeted at a single channel, send to all twitch + // channels + getApp()->getTwitch()->forEachChannelAndSpecialChannels( + [msg](const auto &c) { + c->addMessage(msg, MessageContext::Original); + }); + return; + } + + auto channel = getApp()->getTwitch()->getChannelOrEmpty(channelName); + + if (channel->isEmpty()) + { + qCDebug(chatterinoTwitch) + << "[IrcManager:handleNoticeMessage] Channel" << channelName + << "not found in channel manager"; + return; + } + + QString tags = message->tags().value("msg-id").toString(); + if (tags == "usage_delete") + { + channel->addSystemMessage( + "Usage: /delete - Deletes the specified message. " + "Can't take more than one argument."); + } + else if (tags == "bad_delete_message_error") + { + channel->addSystemMessage( + "There was a problem deleting the message. " + "It might be from another channel or too old to delete."); + } + else if (tags == "host_on" || tags == "host_target_went_offline") + { + bool hostOn = (tags == "host_on"); + QStringList parts = msg->messageText.split(QLatin1Char(' ')); + if ((hostOn && parts.size() != 3) || (!hostOn && parts.size() != 7)) + { return; } - - auto channel = getApp()->getTwitch()->getChannelOrEmpty(channelName); - - if (channel->isEmpty()) + auto &hostedChannelName = hostOn ? parts[2] : parts[0]; + if (hostedChannelName.size() < 2) { - qCDebug(chatterinoTwitch) - << "[IrcManager:handleNoticeMessage] Channel" << channelName - << "not found in channel manager"; return; } - - QString tags = message->tags().value("msg-id").toString(); - if (tags == "usage_delete") + if (hostOn) { - channel->addSystemMessage( - "Usage: /delete - Deletes the specified message. " - "Can't take more than one argument."); + hostedChannelName.chop(1); } - else if (tags == "bad_delete_message_error") - { - channel->addSystemMessage( - "There was a problem deleting the message. " - "It might be from another channel or too old to delete."); - } - else if (tags == "host_on" || tags == "host_target_went_offline") - { - bool hostOn = (tags == "host_on"); - QStringList parts = msg->messageText.split(QLatin1Char(' ')); - if ((hostOn && parts.size() != 3) || (!hostOn && parts.size() != 7)) - { - return; - } - auto &hostedChannelName = hostOn ? parts[2] : parts[0]; - if (hostedChannelName.size() < 2) - { - return; - } - if (hostOn) - { - hostedChannelName.chop(1); - } - channel->addMessage(MessageBuilder::makeHostingSystemMessage( - hostedChannelName, hostOn), - MessageContext::Original); - } - else if (tags == "room_mods" || tags == "vips_success") - { - // /mods and /vips - // room_mods: The moderators of this channel are: ampzyh, antichriststollen, apa420, ... - // vips_success: The VIPs of this channel are: 8008, aiden, botfactory, ... + channel->addMessage( + MessageBuilder::makeHostingSystemMessage(hostedChannelName, hostOn), + MessageContext::Original); + } + else if (tags == "room_mods" || tags == "vips_success") + { + // /mods and /vips + // room_mods: The moderators of this channel are: ampzyh, antichriststollen, apa420, ... + // vips_success: The VIPs of this channel are: 8008, aiden, botfactory, ... - QString noticeText = msg->messageText; - if (tags == "vips_success") - { - // this one has a trailing period, need to get rid of it. - noticeText.chop(1); - } - - QStringList msgParts = noticeText.split(':'); - MessageBuilder builder; - - auto *tc = dynamic_cast(channel.get()); - assert(tc != nullptr && - "IrcMessageHandler::handleNoticeMessage. Twitch specific " - "functionality called in non twitch channel"); - - auto users = msgParts.at(1) - .mid(1) // there is a space before the first user - .split(", "); - users.sort(Qt::CaseInsensitive); - channel->addMessage(MessageBuilder::makeListOfUsersMessage( - msgParts.at(0), users, tc), - MessageContext::Original); - } - else + QString noticeText = msg->messageText; + if (tags == "vips_success") { - channel->addMessage(msg, MessageContext::Original); + // this one has a trailing period, need to get rid of it. + noticeText.chop(1); } + + QStringList msgParts = noticeText.split(':'); + MessageBuilder builder; + + auto *tc = dynamic_cast(channel.get()); + assert(tc != nullptr && + "IrcMessageHandler::handleNoticeMessage. Twitch specific " + "functionality called in non twitch channel"); + + auto users = msgParts.at(1) + .mid(1) // there is a space before the first user + .split(", "); + users.sort(Qt::CaseInsensitive); + channel->addMessage( + MessageBuilder::makeListOfUsersMessage(msgParts.at(0), users, tc), + MessageContext::Original); + } + else + { + channel->addMessage(msg, MessageContext::Original); } } @@ -1405,76 +955,13 @@ void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message) } } -float IrcMessageHandler::similarity( - const MessagePtr &msg, const LimitedQueueSnapshot &messages) -{ - float similarityPercent = 0.0F; - int checked = 0; - - for (size_t i = 1; i <= messages.size(); ++i) - { - if (checked >= getSettings()->hideSimilarMaxMessagesToCheck) - { - break; - } - const auto &prevMsg = messages[messages.size() - i]; - if (prevMsg->parseTime.secsTo(QTime::currentTime()) >= - getSettings()->hideSimilarMaxDelay) - { - break; - } - if (getSettings()->hideSimilarBySameUser && - msg->loginName != prevMsg->loginName) - { - continue; - } - ++checked; - similarityPercent = std::max( - similarityPercent, - relativeSimilarity(msg->messageText, prevMsg->messageText)); - } - - return similarityPercent; -} - -void IrcMessageHandler::setSimilarityFlags(const MessagePtr &message, - const ChannelPtr &channel) -{ - if (getSettings()->similarityEnabled) - { - bool isMyself = - message->loginName == - getApp()->getAccounts()->twitch.getCurrent()->getUserName(); - bool hideMyself = getSettings()->hideSimilarMyself; - - if (isMyself && !hideMyself) - { - return; - } - - if (IrcMessageHandler::similarity(message, - channel->getMessageSnapshot()) > - getSettings()->similarityPercentage) - { - message->flags.set(MessageFlag::Similar, true); - if (getSettings()->colorSimilarDisabled) - { - message->flags.set(MessageFlag::Disabled, true); - } - } - } -} - void IrcMessageHandler::addMessage(Communi::IrcMessage *message, - const ChannelPtr &chan, + MessageSink &sink, TwitchChannel *chan, const QString &originalContent, - ITwitchIrcServer &server, bool isSub, + ITwitchIrcServer &twitch, bool isSub, bool isAction) { - if (chan->isEmpty()) - { - return; - } + assert(chan); MessageParseArgs args; if (isSub) @@ -1489,8 +976,6 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, } args.isAction = isAction; - auto *channel = dynamic_cast(chan.get()); - const auto &tags = message->tags(); QString rewardId; if (const auto it = tags.find("custom-reward-id"); it != tags.end()) @@ -1506,13 +991,16 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, rewardId = msgId; } } - if (!rewardId.isEmpty() && !channel->isChannelPointRewardKnown(rewardId)) + if (!rewardId.isEmpty() && + sink.sinkTraits().has( + MessageSinkTrait::RequiresKnownChannelPointReward) && + !chan->isChannelPointRewardKnown(rewardId)) { // Need to wait for pubsub reward notification qCDebug(chatterinoTwitch) << "TwitchChannel reward added ADD " "callback since reward is not known:" << rewardId; - channel->addQueuedRedemption(rewardId, originalContent, message); + chan->addQueuedRedemption(rewardId, originalContent, message); return; } args.channelPointRewardId = rewardId; @@ -1526,9 +1014,9 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, it != tags.end()) { const QString replyID = it.value().toString(); - auto threadIt = channel->threads().find(replyID); + auto threadIt = chan->threads().find(replyID); std::shared_ptr rootThread; - if (threadIt != channel->threads().end() && !threadIt->second.expired()) + if (threadIt != chan->threads().end() && !threadIt->second.expired()) { // Thread already exists (has a reply) auto thread = threadIt->second.lock(); @@ -1539,7 +1027,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, else { // Thread does not yet exist, find root reply and create thread. - auto root = channel->findMessage(replyID); + auto root = sink.findMessageByID(replyID); if (root) { // Found root reply message @@ -1549,7 +1037,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, replyCtx.thread = newThread; rootThread = newThread; // Store weak reference to thread in channel - channel->addReplyThread(newThread); + chan->addReplyThread(newThread); } } @@ -1566,8 +1054,8 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, } else { - auto parentThreadIt = channel->threads().find(parentID); - if (parentThreadIt != channel->threads().end()) + auto parentThreadIt = chan->threads().find(parentID); + if (parentThreadIt != chan->threads().end()) { auto thread = parentThreadIt->second.lock(); if (thread) @@ -1577,7 +1065,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, } else { - auto parent = channel->findMessage(parentID); + auto parent = sink.findMessageByID(parentID); if (parent) { replyCtx.parent = parent; @@ -1589,7 +1077,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, args.allowIgnore = !isSub; auto [msg, alert] = MessageBuilder::makeIrcMessage( - channel, message, args, content, messageOffset, replyCtx.thread, + chan, message, args, content, messageOffset, replyCtx.thread, replyCtx.parent); if (msg) @@ -1600,29 +1088,27 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, msg->flags.unset(MessageFlag::Highlighted); } - IrcMessageHandler::setSimilarityFlags(msg, chan); + sink.applySimilarityFilters(msg); if (!msg->flags.has(MessageFlag::Similar) || (!getSettings()->hideSimilar && getSettings()->shownSimilarTriggerHighlights)) { - MessageBuilder::triggerHighlights(channel, alert); + MessageBuilder::triggerHighlights(chan, alert); } const auto highlighted = msg->flags.has(MessageFlag::Highlighted); const auto showInMentions = msg->flags.has(MessageFlag::ShowInMentions); - if (highlighted && showInMentions) + if (highlighted && showInMentions && + sink.sinkTraits().has(MessageSinkTrait::AddMentionsToGlobalChannel)) { - server.getMentionsChannel()->addMessage(msg, + twitch.getMentionsChannel()->addMessage(msg, MessageContext::Original); } - chan->addMessage(msg, MessageContext::Original); - if (auto *chatters = dynamic_cast(chan.get())) - { - chatters->addRecentChatter(msg->displayName); - } + sink.addMessage(msg, MessageContext::Original); + chan->addRecentChatter(msg->displayName); } } diff --git a/src/providers/twitch/IrcMessageHandler.hpp b/src/providers/twitch/IrcMessageHandler.hpp index ba6d2d983..60fcacd4b 100644 --- a/src/providers/twitch/IrcMessageHandler.hpp +++ b/src/providers/twitch/IrcMessageHandler.hpp @@ -16,6 +16,7 @@ struct Message; using MessagePtr = std::shared_ptr; class TwitchChannel; class TwitchMessageBuilder; +class MessageSink; struct ClearChatMessage { MessagePtr message; @@ -33,30 +34,34 @@ public: * Parse an IRC message into 0 or more Chatterino messages * Takes previously loaded messages into consideration to add reply contexts **/ - static std::vector parseMessageWithReply( - Channel *channel, Communi::IrcMessage *message, - std::vector &otherLoaded); + static void parseMessageInto(Communi::IrcMessage *message, + MessageSink &sink, TwitchChannel *channel); void handlePrivMessage(Communi::IrcPrivateMessage *message, ITwitchIrcServer &twitchServer); + static void parsePrivMessageInto(Communi::IrcPrivateMessage *message, + MessageSink &sink, TwitchChannel *channel); void handleRoomStateMessage(Communi::IrcMessage *message); void handleClearChatMessage(Communi::IrcMessage *message); void handleClearMessageMessage(Communi::IrcMessage *message); void handleUserStateMessage(Communi::IrcMessage *message); - void handleWhisperMessage(Communi::IrcMessage *ircMessage); + void handleWhisperMessage(Communi::IrcMessage *ircMessage); void handleUserNoticeMessage(Communi::IrcMessage *message, ITwitchIrcServer &twitchServer); + static void parseUserNoticeMessageInto(Communi::IrcMessage *message, + MessageSink &sink, + TwitchChannel *channel); void handleNoticeMessage(Communi::IrcNoticeMessage *message); void handleJoinMessage(Communi::IrcMessage *message); void handlePartMessage(Communi::IrcMessage *message); - void addMessage(Communi::IrcMessage *message, const ChannelPtr &chan, - const QString &originalContent, ITwitchIrcServer &server, - bool isSub, bool isAction); + static void addMessage(Communi::IrcMessage *message, MessageSink &sink, + TwitchChannel *chan, const QString &originalContent, + ITwitchIrcServer &twitch, bool isSub, bool isAction); private: static float similarity(const MessagePtr &msg, diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 93ac4519c..7c7cc1304 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -452,8 +452,8 @@ void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward) if (reward.id == msg.rewardID) { IrcMessageHandler::instance().addMessage( - msg.message.get(), shared_from_this(), - msg.originalContent, *server, false, false); + msg.message.get(), *this, this, msg.originalContent, + *server, false, false); return true; } return false; @@ -1356,8 +1356,6 @@ void TwitchChannel::loadRecentMessages() { msgs.push_back(msg); } - - tc->addRecentChatter(msg->displayName); } getApp()->getTwitch()->getMentionsChannel()->fillInMissingMessages( diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 9ca93f6fd..9e2fe2fbf 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -312,7 +312,7 @@ void TwitchIrcServer::initialize() postToThread([chan, action] { MessageBuilder msg(action); msg->flags.set(MessageFlag::PubSub); - chan->addOrReplaceTimeout(msg.release()); + chan->addOrReplaceTimeout(msg.release(), QTime::currentTime()); }); }); diff --git a/src/util/VectorMessageSink.cpp b/src/util/VectorMessageSink.cpp new file mode 100644 index 000000000..3911fee89 --- /dev/null +++ b/src/util/VectorMessageSink.cpp @@ -0,0 +1,86 @@ +#include "util/VectorMessageSink.hpp" + +#include "messages/MessageSimilarity.hpp" +#include "util/ChannelHelpers.hpp" + +#include + +namespace chatterino { + +VectorMessageSink::VectorMessageSink(MessageSinkTraits traits, + MessageFlags additionalFlags) + : additionalFlags(additionalFlags) + , traits(traits){}; +VectorMessageSink::~VectorMessageSink() = default; + +void VectorMessageSink::addMessage(MessagePtr message, MessageContext ctx, + std::optional overridingFlags) +{ + assert(!overridingFlags.has_value()); + assert(ctx == MessageContext::Original); + + message->flags.set(this->additionalFlags); + this->messages_.emplace_back(std::move(message)); +} + +void VectorMessageSink::addOrReplaceTimeout(MessagePtr clearchatMessage, + QTime now) +{ + addOrReplaceChannelTimeout( + this->messages_, std::move(clearchatMessage), now, + [&](auto idx, auto /*msg*/, auto &&replacement) { + replacement->flags.set(this->additionalFlags); + this->messages_[idx] = replacement; + }, + [&](auto &&msg) { + this->messages_.emplace_back(msg); + }, + false); +} + +void VectorMessageSink::disableAllMessages() +{ + if (this->additionalFlags.has(MessageFlag::RecentMessage)) + { + return; // don't disable recent messages + } + + for (const auto &msg : this->messages_) + { + msg->flags.set(MessageFlag::Disabled); + } +} + +void VectorMessageSink::applySimilarityFilters(const MessagePtr &message) const +{ + setSimilarityFlags(message, this->messages_); +} + +MessagePtr VectorMessageSink::findMessageByID(QStringView id) +{ + for (const auto &msg : this->messages_ | std::views::reverse) + { + if (msg->id == id) + { + return msg; + } + } + return {}; +} + +const std::vector &VectorMessageSink::messages() const +{ + return this->messages_; +} + +std::vector VectorMessageSink::takeMessages() && +{ + return std::move(this->messages_); +} + +MessageSinkTraits VectorMessageSink::sinkTraits() const +{ + return this->traits; +} + +} // namespace chatterino diff --git a/src/util/VectorMessageSink.hpp b/src/util/VectorMessageSink.hpp new file mode 100644 index 000000000..c4ffcfa9c --- /dev/null +++ b/src/util/VectorMessageSink.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "messages/MessageSink.hpp" + +namespace chatterino { + +class VectorMessageSink final : public MessageSink +{ +public: + VectorMessageSink(MessageSinkTraits traits = {}, + MessageFlags additionalFlags = {}); + ~VectorMessageSink() override; + + void addMessage( + MessagePtr message, MessageContext ctx, + std::optional overridingFlags = std::nullopt) override; + void addOrReplaceTimeout(MessagePtr clearchatMessage, QTime now) override; + + void disableAllMessages() override; + + void applySimilarityFilters(const MessagePtr &message) const override; + + MessagePtr findMessageByID(QStringView id) override; + + MessageSinkTraits sinkTraits() const override; + + const std::vector &messages() const; + std::vector takeMessages() &&; + +private: + std::vector messages_; + MessageFlags additionalFlags; + MessageSinkTraits traits; +}; + +} // namespace chatterino diff --git a/tests/snapshots/IrcMessageHandler/announcement.json b/tests/snapshots/IrcMessageHandler/announcement.json index b3ddfd2e6..06b21210c 100644 --- a/tests/snapshots/IrcMessageHandler/announcement.json +++ b/tests/snapshots/IrcMessageHandler/announcement.json @@ -180,6 +180,7 @@ } ], "flags": "Collapsed|Subscription", + "highlightColor": "#64c466ff", "id": "8c26e1ab-b50c-4d9d-bc11-3fd57a941d90", "localizedName": "", "loginName": "supinic", diff --git a/tests/snapshots/IrcMessageHandler/reply-child.json b/tests/snapshots/IrcMessageHandler/reply-child.json index 8862f7738..be8a17317 100644 --- a/tests/snapshots/IrcMessageHandler/reply-child.json +++ b/tests/snapshots/IrcMessageHandler/reply-child.json @@ -80,7 +80,7 @@ "trailingSpace": true, "type": "SingleLineTextElement", "words": [ - "a" + "b" ] }, { @@ -169,6 +169,7 @@ "localizedName": "", "loginName": "nerixyz", "messageText": "c", + "replyParent": "474f19ab-a1b0-410a-877a-5b0e2ae8be6d", "replyThread": { "replies": [ "474f19ab-a1b0-410a-877a-5b0e2ae8be6d", diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json b/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json index a6877c365..f612fd720 100644 --- a/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json +++ b/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json @@ -256,6 +256,7 @@ } ], "flags": "Collapsed|Subscription|SharedMessage", + "highlightColor": "#64c466ff", "id": "01cd601f-bc3f-49d5-ab4b-136fa9d6ec22", "localizedName": "", "loginName": "lahoooo", diff --git a/tests/snapshots/IrcMessageHandler/sub-message.json b/tests/snapshots/IrcMessageHandler/sub-message.json index fd74777c5..98c787647 100644 --- a/tests/snapshots/IrcMessageHandler/sub-message.json +++ b/tests/snapshots/IrcMessageHandler/sub-message.json @@ -243,6 +243,7 @@ } ], "flags": "Collapsed|Subscription", + "highlightColor": "#64c466ff", "id": "db25007f-7a18-43eb-9379-80131e44d633", "localizedName": "", "loginName": "ronni", diff --git a/tests/src/IrcMessageHandler.cpp b/tests/src/IrcMessageHandler.cpp index a80e928ef..8ed4ee568 100644 --- a/tests/src/IrcMessageHandler.cpp +++ b/tests/src/IrcMessageHandler.cpp @@ -27,6 +27,7 @@ #include "singletons/Emotes.hpp" #include "Test.hpp" #include "util/IrcHelpers.hpp" +#include "util/VectorMessageSink.hpp" #include #include @@ -572,19 +573,14 @@ TEST_P(TestIrcMessageHandlerP, Run) { auto channel = makeMockTwitchChannel(u"pajlada"_s, *snapshot); - std::vector prevMessages; + VectorMessageSink sink; for (auto prevInput : snapshot->param("prevMessages").toArray()) { auto *ircMessage = Communi::IrcMessage::fromData( prevInput.toString().toUtf8(), nullptr); ASSERT_NE(ircMessage, nullptr); - auto builtMessages = IrcMessageHandler::parseMessageWithReply( - channel.get(), ircMessage, prevMessages); - for (const auto &builtMessage : builtMessages) - { - prevMessages.emplace_back(builtMessage); - } + IrcMessageHandler::parseMessageInto(ircMessage, sink, channel.get()); delete ircMessage; } @@ -592,13 +588,13 @@ TEST_P(TestIrcMessageHandlerP, Run) Communi::IrcMessage::fromData(snapshot->inputUtf8(), nullptr); ASSERT_NE(ircMessage, nullptr); - auto builtMessages = IrcMessageHandler::parseMessageWithReply( - channel.get(), ircMessage, prevMessages); + auto firstAddedMsg = sink.messages().size(); + IrcMessageHandler::parseMessageInto(ircMessage, sink, channel.get()); QJsonArray got; - for (const auto &msg : builtMessages) + for (auto i = firstAddedMsg; i < sink.messages().size(); i++) { - got.append(msg->toJson()); + got.append(sink.messages()[i]->toJson()); } delete ircMessage; From db8047ea7b0ec4deba86722c484e524637b7f6fe Mon Sep 17 00:00:00 2001 From: hemirt <1310440+hemirt@users.noreply.github.com> Date: Sat, 2 Nov 2024 14:21:56 +0100 Subject: [PATCH 37/40] highlight tabs only on unviewed messages (#5649) --- CHANGELOG.md | 1 + src/widgets/Notebook.cpp | 4 +- src/widgets/helper/ChannelView.cpp | 27 ++++ src/widgets/helper/ChannelView.hpp | 13 +- src/widgets/helper/NotebookTab.cpp | 210 +++++++++++++++++++++++++- src/widgets/helper/NotebookTab.hpp | 28 +++- src/widgets/splits/SplitContainer.cpp | 12 +- 7 files changed, 286 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc3924f39..95466ae93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ - Minor: Indicate when subscriptions and resubscriptions are for multiple months. (#5642) - Minor: Proxy URL information is now included in the `/debug-env` command. (#5648) - Minor: Make raid entry message usernames clickable. (#5651) +- Minor: Tabs unhighlight when their content is read in other tabs. (#5649) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426, #5612) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed restricted users usernames not being clickable. (#5405) diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index 2a01020fc..53c6dfcfa 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -204,7 +204,7 @@ void Notebook::duplicatePage(QWidget *page) { newTabPosition = tabPosition + 1; } - auto newTabHighlightState = item->tab->highlightState(); + QString newTabTitle = ""; if (item->tab->hasCustomTitle()) { @@ -213,7 +213,7 @@ void Notebook::duplicatePage(QWidget *page) auto *tab = this->addPageAt(newContainer, newTabPosition, newTabTitle, false); - tab->setHighlightState(newTabHighlightState); + tab->copyHighlightStateAndSourcesFrom(item->tab); newContainer->setTab(tab); } diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index a5fa62582..22cb629c4 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -1064,6 +1064,8 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) this->underlyingChannel_ = underlyingChannel; + this->updateID(); + this->performLayout(); this->queueUpdate(); @@ -1082,6 +1084,8 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) void ChannelView::setFilters(const QList &ids) { this->channelFilters_ = std::make_shared(ids); + + this->updateID(); } QList ChannelView::getFilterIds() const @@ -3243,4 +3247,27 @@ void ChannelView::pendingLinkInfoStateChanged() this->tooltipWidget_->applyLastBoundsCheck(); } +void ChannelView::updateID() +{ + if (!this->underlyingChannel_) + { + // cannot update + return; + } + + std::size_t seed = 0; + auto first = qHash(this->underlyingChannel_->getName()); + auto second = qHash(this->getFilterIds()); + + boost::hash_combine(seed, first); + boost::hash_combine(seed, second); + + this->id_ = seed; +} + +ChannelView::ChannelViewID ChannelView::getID() const +{ + return this->id_; +} + } // namespace chatterino diff --git a/src/widgets/helper/ChannelView.hpp b/src/widgets/helper/ChannelView.hpp index 40b40444e..3c8f078f4 100644 --- a/src/widgets/helper/ChannelView.hpp +++ b/src/widgets/helper/ChannelView.hpp @@ -212,6 +212,14 @@ public: Scrollbar *scrollbar(); + using ChannelViewID = std::size_t; + /// + /// \brief Get the ID of this ChannelView + /// + /// The ID is made of the underlying channel's name + /// combined with the filter set IDs + ChannelViewID getID() const; + pajlada::Signals::Signal mouseDown; pajlada::Signals::NoArgSignal selectionChanged; pajlada::Signals::Signal tabHighlightRequested; @@ -315,6 +323,9 @@ private: void showReplyThreadPopup(const MessagePtr &message); bool canReplyToMessages() const; + void updateID(); + ChannelViewID id_{}; + bool layoutQueued_ = false; bool bufferInvalidationQueued_ = false; @@ -376,7 +387,7 @@ private: FilterSetPtr channelFilters_; // Returns true if message should be included - bool shouldIncludeMessage(const MessagePtr &m) const; + bool shouldIncludeMessage(const MessagePtr &message) const; // Returns whether the scrollbar should have highlights bool showScrollbarHighlights() const; diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index be2b371a3..563084d39 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -1,6 +1,7 @@ #include "widgets/helper/NotebookTab.hpp" #include "Application.hpp" +#include "common/Channel.hpp" #include "common/Common.hpp" #include "controllers/hotkeys/HotkeyCategory.hpp" #include "controllers/hotkeys/HotkeyController.hpp" @@ -12,9 +13,11 @@ #include "widgets/dialogs/SettingsDialog.hpp" #include "widgets/Notebook.hpp" #include "widgets/splits/DraggedSplit.hpp" +#include "widgets/splits/Split.hpp" #include "widgets/splits/SplitContainer.hpp" #include +#include #include #include #include @@ -302,10 +305,134 @@ bool NotebookTab::isSelected() const return this->selected_; } +void NotebookTab::removeHighlightStateChangeSources( + const HighlightSources &toRemove) +{ + for (const auto &[source, _] : toRemove) + { + this->removeHighlightSource(source); + } +} + +void NotebookTab::removeHighlightSource( + const ChannelView::ChannelViewID &source) +{ + this->highlightSources_.erase(source); +} + +void NotebookTab::newHighlightSourceAdded(const ChannelView &channelViewSource) +{ + auto channelViewId = channelViewSource.getID(); + this->removeHighlightSource(channelViewId); + this->updateHighlightStateDueSourcesChange(); + + auto *splitNotebook = dynamic_cast(this->notebook_); + if (splitNotebook) + { + for (int i = 0; i < splitNotebook->getPageCount(); ++i) + { + auto *splitContainer = + dynamic_cast(splitNotebook->getPageAt(i)); + if (splitContainer) + { + auto *tab = splitContainer->getTab(); + if (tab && tab != this) + { + tab->removeHighlightSource(channelViewId); + tab->updateHighlightStateDueSourcesChange(); + } + } + } + } +} + +void NotebookTab::updateHighlightStateDueSourcesChange() +{ + if (std::ranges::any_of(this->highlightSources_, [](const auto &keyval) { + return keyval.second == HighlightState::Highlighted; + })) + { + assert(this->highlightState_ == HighlightState::Highlighted); + return; + } + + if (std::ranges::any_of(this->highlightSources_, [](const auto &keyval) { + return keyval.second == HighlightState::NewMessage; + })) + { + if (this->highlightState_ != HighlightState::NewMessage) + { + this->highlightState_ = HighlightState::NewMessage; + this->update(); + } + } + else + { + if (this->highlightState_ != HighlightState::None) + { + this->highlightState_ = HighlightState::None; + this->update(); + } + } + + assert(this->highlightState_ != HighlightState::Highlighted); +} + +void NotebookTab::copyHighlightStateAndSourcesFrom(const NotebookTab *sourceTab) +{ + if (this->isSelected()) + { + assert(this->highlightSources_.empty()); + assert(this->highlightState_ == HighlightState::None); + return; + } + + this->highlightSources_ = sourceTab->highlightSources_; + + if (!this->highlightEnabled_ && + sourceTab->highlightState_ == HighlightState::NewMessage) + { + return; + } + + if (this->highlightState_ == sourceTab->highlightState_ || + this->highlightState_ == HighlightState::Highlighted) + { + return; + } + + this->highlightState_ = sourceTab->highlightState_; + this->update(); +} + void NotebookTab::setSelected(bool value) { this->selected_ = value; + if (value) + { + auto *splitNotebook = dynamic_cast(this->notebook_); + if (splitNotebook) + { + for (int i = 0; i < splitNotebook->getPageCount(); ++i) + { + auto *splitContainer = + dynamic_cast(splitNotebook->getPageAt(i)); + if (splitContainer) + { + auto *tab = splitContainer->getTab(); + if (tab && tab != this) + { + tab->removeHighlightStateChangeSources( + this->highlightSources_); + tab->updateHighlightStateDueSourcesChange(); + } + } + } + } + } + + this->highlightSources_.clear(); this->highlightState_ = HighlightState::None; this->update(); @@ -358,13 +485,22 @@ bool NotebookTab::isLive() const return this->isLive_; } +HighlightState NotebookTab::highlightState() const +{ + return this->highlightState_; +} + void NotebookTab::setHighlightState(HighlightState newHighlightStyle) { if (this->isSelected()) { + assert(this->highlightSources_.empty()); + assert(this->highlightState_ == HighlightState::None); return; } + this->highlightSources_.clear(); + if (!this->highlightEnabled_ && newHighlightStyle == HighlightState::NewMessage) { @@ -381,9 +517,79 @@ void NotebookTab::setHighlightState(HighlightState newHighlightStyle) this->update(); } -HighlightState NotebookTab::highlightState() const +void NotebookTab::updateHighlightState(HighlightState newHighlightStyle, + const ChannelView &channelViewSource) { - return this->highlightState_; + if (this->isSelected()) + { + assert(this->highlightSources_.empty()); + assert(this->highlightState_ == HighlightState::None); + return; + } + + if (!this->shouldMessageHighlight(channelViewSource)) + { + return; + } + + if (!this->highlightEnabled_ && + newHighlightStyle == HighlightState::NewMessage) + { + return; + } + + // message is highlighting unvisible tab + + auto channelViewId = channelViewSource.getID(); + + switch (newHighlightStyle) + { + case HighlightState::Highlighted: + // override lower states + this->highlightSources_.insert_or_assign(channelViewId, + newHighlightStyle); + case HighlightState::NewMessage: { + // only insert if no state already there to avoid overriding + if (!this->highlightSources_.contains(channelViewId)) + { + this->highlightSources_.emplace(channelViewId, + newHighlightStyle); + } + break; + } + case HighlightState::None: + break; + } + + if (this->highlightState_ == newHighlightStyle || + this->highlightState_ == HighlightState::Highlighted) + { + return; + } + + this->highlightState_ = newHighlightStyle; + this->update(); +} + +bool NotebookTab::shouldMessageHighlight( + const ChannelView &channelViewSource) const +{ + auto *visibleSplitContainer = + dynamic_cast(this->notebook_->getSelectedPage()); + if (visibleSplitContainer != nullptr) + { + const auto &visibleSplits = visibleSplitContainer->getSplits(); + for (const auto &visibleSplit : visibleSplits) + { + if (channelViewSource.getID() == + visibleSplit->getChannelView().getID()) + { + return false; + } + } + } + + return true; } void NotebookTab::setHighlightsEnabled(const bool &newVal) diff --git a/src/widgets/helper/NotebookTab.hpp b/src/widgets/helper/NotebookTab.hpp index 6ae7802d0..ae3bfbc2f 100644 --- a/src/widgets/helper/NotebookTab.hpp +++ b/src/widgets/helper/NotebookTab.hpp @@ -2,6 +2,7 @@ #include "common/Common.hpp" #include "widgets/helper/Button.hpp" +#include "widgets/helper/ChannelView.hpp" #include "widgets/Notebook.hpp" #include @@ -59,11 +60,24 @@ public: **/ bool isLive() const; + /** + * @brief Sets the highlight state of this tab clearing highlight sources + * + * Obeys the HighlightsEnabled setting and highlight states hierarchy + */ void setHighlightState(HighlightState style); - HighlightState highlightState() const; - + /** + * @brief Updates the highlight state and highlight sources of this tab + * + * Obeys the HighlightsEnabled setting and the highlight state hierarchy and tracks the highlight state update sources + */ + void updateHighlightState(HighlightState style, + const ChannelView &channelViewSource); + void copyHighlightStateAndSourcesFrom(const NotebookTab *sourceTab); void setHighlightsEnabled(const bool &newVal); + void newHighlightSourceAdded(const ChannelView &channelViewSource); bool hasHighlightsEnabled() const; + HighlightState highlightState() const; void moveAnimated(QPoint targetPos, bool animated = true); @@ -107,6 +121,16 @@ private: int normalTabWidthForHeight(int height) const; + bool shouldMessageHighlight(const ChannelView &channelViewSource) const; + + using HighlightSources = + std::unordered_map; + HighlightSources highlightSources_; + + void removeHighlightStateChangeSources(const HighlightSources &toRemove); + void removeHighlightSource(const ChannelView::ChannelViewID &source); + void updateHighlightStateDueSourcesChange(); + QPropertyAnimation positionChangedAnimation_; QPoint positionAnimationDesiredPoint_; diff --git a/src/widgets/splits/SplitContainer.cpp b/src/widgets/splits/SplitContainer.cpp index 1cca478f0..430098f0e 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -214,13 +214,21 @@ void SplitContainer::addSplit(Split *split) auto &&conns = this->connectionsPerSplit_[split]; conns.managedConnect(split->getChannelView().tabHighlightRequested, - [this](HighlightState state) { + [this, split](HighlightState state) { if (this->tab_ != nullptr) { - this->tab_->setHighlightState(state); + this->tab_->updateHighlightState( + state, split->getChannelView()); } }); + conns.managedConnect(split->channelChanged, [this, split] { + if (this->tab_ != nullptr) + { + this->tab_->newHighlightSourceAdded(split->getChannelView()); + } + }); + conns.managedConnect(split->getChannelView().liveStatusChanged, [this]() { this->refreshTabLiveStatus(); }); From 4b725c4d6b52bbeb1c86bc542ade8f0c366fcc73 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 2 Nov 2024 22:32:49 +0100 Subject: [PATCH 38/40] fix(7TV): ignore `entitlement.reset` (#5685) --- CHANGELOG.md | 1 + src/providers/seventv/SeventvEventAPI.cpp | 4 ++++ src/providers/seventv/eventapi/Subscription.hpp | 3 +++ 3 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95466ae93..8c433308b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,7 @@ - Dev: Refactored IRC message building. (#5663) - Dev: Fixed some compiler warnings. (#5672) - Dev: Unified parsing of historic and live IRC messages. (#5678) +- Dev: 7TV's `entitlement.reset` is now explicitly ignored. (#5685) ## 2.5.1 diff --git a/src/providers/seventv/SeventvEventAPI.cpp b/src/providers/seventv/SeventvEventAPI.cpp index fcb99ce12..0a7797eb7 100644 --- a/src/providers/seventv/SeventvEventAPI.cpp +++ b/src/providers/seventv/SeventvEventAPI.cpp @@ -233,6 +233,10 @@ void SeventvEventAPI::handleDispatch(const Dispatch &dispatch) } } break; + case SubscriptionType::ResetEntitlement: { + // unhandled (not clear what we'd do here yet) + } + break; default: { qCDebug(chatterinoSeventvEventAPI) << "Unknown subscription type:" diff --git a/src/providers/seventv/eventapi/Subscription.hpp b/src/providers/seventv/eventapi/Subscription.hpp index 65cf03544..c6767b139 100644 --- a/src/providers/seventv/eventapi/Subscription.hpp +++ b/src/providers/seventv/eventapi/Subscription.hpp @@ -27,6 +27,7 @@ enum class SubscriptionType { CreateEntitlement, UpdateEntitlement, DeleteEntitlement, + ResetEntitlement, INVALID, }; @@ -119,6 +120,8 @@ constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< return "entitlement.update"; case SubscriptionType::DeleteEntitlement: return "entitlement.delete"; + case SubscriptionType::ResetEntitlement: + return "entitlement.reset"; default: return default_tag; From 403fc6d3c4397ac0a9459a499f975f09d44f4c8c Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 3 Nov 2024 11:49:00 +0100 Subject: [PATCH 39/40] fix: ensure timer doesn't run if the QuickSwitcherPopup is dead (#5687) --- CHANGELOG.md | 1 + src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c433308b..a66013510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ - Bugfix: Fixed event emotes not showing up in autocomplete and popups. (#5239, #5580, #5582, #5632) - Bugfix: Fixed tab visibility being controllable in the emote popup. (#5530) - Bugfix: Fixed account switch not being saved if no other settings were changed. (#5558) +- Bugfix: Fixed a crash that could occur when handling the quick switcher popup really quickly. (#5687) - Bugfix: Fixed 7TV badges being inadvertently animated. (#5674) - Bugfix: Fixed some tooltips not being readable. (#5578) - Bugfix: Fixed log files being locked longer than needed. (#5592) diff --git a/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp b/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp index 7841d06b4..b0294c45a 100644 --- a/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp +++ b/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp @@ -141,7 +141,7 @@ void QuickSwitcherPopup::updateSuggestions(const QString &text) * Timeout interval 0 means the call will be delayed until all window events * have been processed (cf. https://doc.qt.io/qt-5/qtimer.html#interval-prop). */ - QTimer::singleShot(0, [this] { + QTimer::singleShot(0, this, [this] { this->adjustSize(); }); } From 8220a1fbd4880f7b9f6fc2b599696444b2363968 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 3 Nov 2024 12:17:16 +0100 Subject: [PATCH 40/40] feat: make usernames in USERNOTICEs clickable (#5686) --- CHANGELOG.md | 1 + src/messages/MessageBuilder.cpp | 103 +++++++++++++++--- src/messages/MessageBuilder.hpp | 15 ++- src/providers/twitch/IrcMessageHandler.cpp | 75 +++++++------ .../IrcMessageHandler/bitsbadge.json | 9 +- .../IrcMessageHandler/sub-first-gift.json | 29 ++++- .../IrcMessageHandler/sub-first-time.json | 9 +- .../snapshots/IrcMessageHandler/sub-gift.json | 29 ++++- .../IrcMessageHandler/sub-message.json | 9 +- .../sub-multi-month-anon-gift.json | 20 +++- .../sub-multi-month-gift-count.json | 29 ++++- .../sub-multi-month-gift.json | 29 ++++- .../sub-multi-month-resub.json | 9 +- .../IrcMessageHandler/sub-multi-month.json | 9 +- .../IrcMessageHandler/sub-no-message.json | 18 ++- 15 files changed, 297 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a66013510..6ceaf7551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ - Minor: Proxy URL information is now included in the `/debug-env` command. (#5648) - Minor: Make raid entry message usernames clickable. (#5651) - Minor: Tabs unhighlight when their content is read in other tabs. (#5649) +- Minor: Made usernames in bits and sub messages clickable. (#5686) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426, #5612) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed restricted users usernames not being clickable. (#5405) diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index cee5a7c87..f88a7c499 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -75,6 +75,8 @@ const QRegularExpression mentionRegex("^@" + regexHelpString); // if findAllUsernames setting is enabled, matches strings like in the examples above, but without @ symbol at the beginning const QRegularExpression allUsernamesMentionRegex("^" + regexHelpString); +const QRegularExpression SPACE_REGEX("\\s"); + const QSet zeroWidthEmotes{ "SoSnowy", "IceCold", "SantaHat", "TopHat", "ReinDeer", "CandyCane", "cvMask", "cvHazmat", @@ -512,7 +514,7 @@ MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text, // check system message for links // (e.g. needed for sub ticket message in sub only mode) const QStringList textFragments = - text.split(QRegularExpression("\\s"), Qt::SkipEmptyParts); + text.split(SPACE_REGEX, Qt::SkipEmptyParts); for (const auto &word : textFragments) { auto link = linkparser::parse(word); @@ -531,33 +533,100 @@ MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text, this->message().searchText = text; } -MessageBuilder::MessageBuilder(RaidEntryMessageTag, const QString &text, - const QString &loginName, - const QString &displayName, - const MessageColor &userColor, const QTime &time) - : MessageBuilder() +MessagePtrMut MessageBuilder::makeSystemMessageWithUser( + const QString &text, const QString &loginName, const QString &displayName, + const MessageColor &userColor, const QTime &time) { - this->emplace(time); + MessageBuilder builder; + builder.emplace(time); - const QStringList textFragments = - text.split(QRegularExpression("\\s"), Qt::SkipEmptyParts); + const auto textFragments = text.split(SPACE_REGEX, Qt::SkipEmptyParts); for (const auto &word : textFragments) { if (word == displayName) { - this->emplace(displayName, loginName, - MessageColor::System, userColor); + builder.emplace(displayName, loginName, + MessageColor::System, userColor); continue; } - this->emplace(word, MessageElementFlag::Text, - MessageColor::System); + builder.emplace(word, MessageElementFlag::Text, + MessageColor::System); } - this->message().flags.set(MessageFlag::System); - this->message().flags.set(MessageFlag::DoNotTriggerNotification); - this->message().messageText = text; - this->message().searchText = text; + builder->flags.set(MessageFlag::System); + builder->flags.set(MessageFlag::DoNotTriggerNotification); + builder->messageText = text; + builder->searchText = text; + + return builder.release(); +} + +MessagePtrMut MessageBuilder::makeSubgiftMessage(const QString &text, + const QVariantMap &tags, + const QTime &time) +{ + MessageBuilder builder; + builder.emplace(time); + + auto gifterLogin = tags.value("login").toString(); + auto gifterDisplayName = tags.value("display-name").toString(); + if (gifterDisplayName.isEmpty()) + { + gifterDisplayName = gifterLogin; + } + MessageColor gifterColor = MessageColor::System; + if (auto colorTag = tags.value("color").value(); colorTag.isValid()) + { + gifterColor = MessageColor(colorTag); + } + + auto recipientLogin = + tags.value("msg-param-recipient-user-name").toString(); + if (recipientLogin.isEmpty()) + { + recipientLogin = tags.value("msg-param-recipient-name").toString(); + } + auto recipientDisplayName = + tags.value("msg-param-recipient-display-name").toString(); + if (recipientDisplayName.isEmpty()) + { + recipientDisplayName = recipientLogin; + } + + const auto textFragments = text.split(SPACE_REGEX, Qt::SkipEmptyParts); + for (const auto &word : textFragments) + { + if (word == gifterDisplayName) + { + builder.emplace(gifterDisplayName, gifterLogin, + MessageColor::System, gifterColor); + continue; + } + if (word.endsWith('!') && + word.size() == recipientDisplayName.size() + 1 && + word.startsWith(recipientDisplayName)) + { + builder + .emplace(recipientDisplayName, recipientLogin, + MessageColor::System, + MessageColor::System) + ->setTrailingSpace(false); + builder.emplace(u"!"_s, MessageElementFlag::Text, + MessageColor::System); + continue; + } + + builder.emplace(word, MessageElementFlag::Text, + MessageColor::System); + } + + builder->flags.set(MessageFlag::System); + builder->flags.set(MessageFlag::DoNotTriggerNotification); + builder->messageText = text; + builder->searchText = text; + + return builder.release(); } MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &timeoutUser, diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 45b65095d..122348b06 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -52,8 +52,6 @@ namespace linkparser { struct SystemMessageTag { }; -struct RaidEntryMessageTag { -}; struct TimeoutMessageTag { }; struct LiveUpdatesUpdateEmoteMessageTag { @@ -69,7 +67,6 @@ struct ImageUploaderResultTag { // NOLINTBEGIN(readability-identifier-naming) const SystemMessageTag systemMessage{}; -const RaidEntryMessageTag raidEntryMessage{}; const TimeoutMessageTag timeoutMessage{}; const LiveUpdatesUpdateEmoteMessageTag liveUpdatesUpdateEmoteMessage{}; const LiveUpdatesRemoveEmoteMessageTag liveUpdatesRemoveEmoteMessage{}; @@ -109,9 +106,6 @@ public: MessageBuilder(SystemMessageTag, const QString &text, const QTime &time = QTime::currentTime()); - MessageBuilder(RaidEntryMessageTag, const QString &text, - const QString &loginName, const QString &displayName, - const MessageColor &userColor, const QTime &time); MessageBuilder(TimeoutMessageTag, const QString &timeoutUser, const QString &sourceUser, const QString &systemMessageText, int times, const QTime &time = QTime::currentTime()); @@ -255,6 +249,15 @@ public: const std::shared_ptr &thread = {}, const MessagePtr &parent = {}); + static MessagePtrMut makeSystemMessageWithUser( + const QString &text, const QString &loginName, + const QString &displayName, const MessageColor &userColor, + const QTime &time); + + static MessagePtrMut makeSubgiftMessage(const QString &text, + const QVariantMap &tags, + const QTime &time); + private: struct TextState { TwitchChannel *twitchChannel = nullptr; diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 7b05fea48..9864b3644 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -699,35 +699,6 @@ void IrcMessageHandler::parseUserNoticeMessageInto(Communi::IrcMessage *message, { messageText = "Announcement"; } - else if (msgType == "raid") - { - auto login = tags.value("login").toString(); - auto displayName = tags.value("msg-param-displayName").toString(); - - if (!login.isEmpty() && !displayName.isEmpty()) - { - MessageColor color = MessageColor::System; - if (auto colorTag = tags.value("color").value(); - colorTag.isValid()) - { - color = MessageColor(colorTag); - } - - auto b = MessageBuilder( - raidEntryMessage, parseTagString(messageText), login, - displayName, color, calculateMessageTime(message).time()); - - b->flags.set(MessageFlag::Subscription); - if (mirrored) - { - b->flags.set(MessageFlag::SharedMessage); - } - auto newMessage = b.release(); - - sink.addMessage(newMessage, MessageContext::Original); - return; - } - } else if (msgType == "subgift") { if (auto monthsIt = tags.find("msg-param-gift-months"); @@ -762,6 +733,20 @@ void IrcMessageHandler::parseUserNoticeMessageInto(Communi::IrcMessage *message, } } } + + // subgifts are special because they include two users + auto msg = MessageBuilder::makeSubgiftMessage( + parseTagString(messageText), tags, + calculateMessageTime(message).time()); + + msg->flags.set(MessageFlag::Subscription); + if (mirrored) + { + msg->flags.set(MessageFlag::SharedMessage); + } + + sink.addMessage(msg, MessageContext::Original); + return; } else if (msgType == "sub" || msgType == "resub") { @@ -795,17 +780,37 @@ void IrcMessageHandler::parseUserNoticeMessageInto(Communi::IrcMessage *message, } } - auto b = MessageBuilder(systemMessage, parseTagString(messageText), - calculateMessageTime(message).time()); + auto displayName = [&] { + if (msgType == u"raid") + { + return tags.value("msg-param-displayName").toString(); + } + return tags.value("display-name").toString(); + }(); + auto login = tags.value("login").toString(); + if (displayName.isEmpty()) + { + displayName = login; + } - b->flags.set(MessageFlag::Subscription); + MessageColor userColor = MessageColor::System; + if (auto colorTag = tags.value("color").value(); + colorTag.isValid()) + { + userColor = MessageColor(colorTag); + } + + auto msg = MessageBuilder::makeSystemMessageWithUser( + parseTagString(messageText), login, displayName, userColor, + calculateMessageTime(message).time()); + + msg->flags.set(MessageFlag::Subscription); if (mirrored) { - b->flags.set(MessageFlag::SharedMessage); + msg->flags.set(MessageFlag::SharedMessage); } - auto newMessage = b.release(); - sink.addMessage(newMessage, MessageContext::Original); + sink.addMessage(msg, MessageContext::Original); } } diff --git a/tests/snapshots/IrcMessageHandler/bitsbadge.json b/tests/snapshots/IrcMessageHandler/bitsbadge.json index bd2b35324..0ebbeb0ec 100644 --- a/tests/snapshots/IrcMessageHandler/bitsbadge.json +++ b/tests/snapshots/IrcMessageHandler/bitsbadge.json @@ -38,8 +38,9 @@ "type": "TimestampElement" }, { - "color": "System", - "flags": "Text", + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", "link": { "type": "None", "value": "" @@ -47,7 +48,9 @@ "style": "ChatMedium", "tooltip": "", "trailingSpace": true, - "type": "TextElement", + "type": "MentionElement", + "userColor": "#ffff4500", + "userLoginName": "whoopiix", "words": [ "whoopiix" ] diff --git a/tests/snapshots/IrcMessageHandler/sub-first-gift.json b/tests/snapshots/IrcMessageHandler/sub-first-gift.json index 25ed044e1..9ad6339fa 100644 --- a/tests/snapshots/IrcMessageHandler/sub-first-gift.json +++ b/tests/snapshots/IrcMessageHandler/sub-first-gift.json @@ -38,8 +38,9 @@ "type": "TimestampElement" }, { - "color": "System", - "flags": "Text", + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", "link": { "type": "None", "value": "" @@ -47,7 +48,9 @@ "style": "ChatMedium", "tooltip": "", "trailingSpace": true, - "type": "TextElement", + "type": "MentionElement", + "userColor": "#ff00ff7f", + "userLoginName": "hyperbolicxd", "words": [ "hyperbolicxd" ] @@ -142,6 +145,24 @@ "to" ] }, + { + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": false, + "type": "MentionElement", + "userColor": "System", + "userLoginName": "quote_if_nam", + "words": [ + "quote_if_nam" + ] + }, { "color": "System", "flags": "Text", @@ -154,7 +175,7 @@ "trailingSpace": true, "type": "TextElement", "words": [ - "quote_if_nam!" + "!" ] }, { diff --git a/tests/snapshots/IrcMessageHandler/sub-first-time.json b/tests/snapshots/IrcMessageHandler/sub-first-time.json index e1427a8b0..8fd22c06a 100644 --- a/tests/snapshots/IrcMessageHandler/sub-first-time.json +++ b/tests/snapshots/IrcMessageHandler/sub-first-time.json @@ -38,8 +38,9 @@ "type": "TimestampElement" }, { - "color": "System", - "flags": "Text", + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", "link": { "type": "None", "value": "" @@ -47,7 +48,9 @@ "style": "ChatMedium", "tooltip": "", "trailingSpace": true, - "type": "TextElement", + "type": "MentionElement", + "userColor": "#ff0000ff", + "userLoginName": "byebyeheart", "words": [ "byebyeheart" ] diff --git a/tests/snapshots/IrcMessageHandler/sub-gift.json b/tests/snapshots/IrcMessageHandler/sub-gift.json index 738fe14e4..f7c77da85 100644 --- a/tests/snapshots/IrcMessageHandler/sub-gift.json +++ b/tests/snapshots/IrcMessageHandler/sub-gift.json @@ -38,8 +38,9 @@ "type": "TimestampElement" }, { - "color": "System", - "flags": "Text", + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", "link": { "type": "None", "value": "" @@ -47,7 +48,9 @@ "style": "ChatMedium", "tooltip": "", "trailingSpace": true, - "type": "TextElement", + "type": "MentionElement", + "userColor": "#ff0000ff", + "userLoginName": "tww2", "words": [ "TWW2" ] @@ -142,6 +145,24 @@ "to" ] }, + { + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": false, + "type": "MentionElement", + "userColor": "System", + "userLoginName": "mr_woodchuck", + "words": [ + "Mr_Woodchuck" + ] + }, { "color": "System", "flags": "Text", @@ -154,7 +175,7 @@ "trailingSpace": true, "type": "TextElement", "words": [ - "Mr_Woodchuck!" + "!" ] } ], diff --git a/tests/snapshots/IrcMessageHandler/sub-message.json b/tests/snapshots/IrcMessageHandler/sub-message.json index 98c787647..8afca0272 100644 --- a/tests/snapshots/IrcMessageHandler/sub-message.json +++ b/tests/snapshots/IrcMessageHandler/sub-message.json @@ -290,8 +290,9 @@ "type": "TimestampElement" }, { - "color": "System", - "flags": "Text", + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", "link": { "type": "None", "value": "" @@ -299,7 +300,9 @@ "style": "ChatMedium", "tooltip": "", "trailingSpace": true, - "type": "TextElement", + "type": "MentionElement", + "userColor": "#ff008000", + "userLoginName": "ronni", "words": [ "ronni" ] diff --git a/tests/snapshots/IrcMessageHandler/sub-multi-month-anon-gift.json b/tests/snapshots/IrcMessageHandler/sub-multi-month-anon-gift.json index a98575565..02fd37398 100644 --- a/tests/snapshots/IrcMessageHandler/sub-multi-month-anon-gift.json +++ b/tests/snapshots/IrcMessageHandler/sub-multi-month-anon-gift.json @@ -217,6 +217,24 @@ "to" ] }, + { + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": false, + "type": "MentionElement", + "userColor": "System", + "userLoginName": "mohammadrezadh", + "words": [ + "MohammadrezaDH" + ] + }, { "color": "System", "flags": "Text", @@ -229,7 +247,7 @@ "trailingSpace": true, "type": "TextElement", "words": [ - "MohammadrezaDH!" + "!" ] } ], diff --git a/tests/snapshots/IrcMessageHandler/sub-multi-month-gift-count.json b/tests/snapshots/IrcMessageHandler/sub-multi-month-gift-count.json index 3b9d3b106..030f29845 100644 --- a/tests/snapshots/IrcMessageHandler/sub-multi-month-gift-count.json +++ b/tests/snapshots/IrcMessageHandler/sub-multi-month-gift-count.json @@ -38,8 +38,9 @@ "type": "TimestampElement" }, { - "color": "System", - "flags": "Text", + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", "link": { "type": "None", "value": "" @@ -47,7 +48,9 @@ "style": "ChatMedium", "tooltip": "", "trailingSpace": true, - "type": "TextElement", + "type": "MentionElement", + "userColor": "#ffff8ea3", + "userLoginName": "inatsufn", "words": [ "iNatsuFN" ] @@ -187,6 +190,24 @@ "to" ] }, + { + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": false, + "type": "MentionElement", + "userColor": "System", + "userLoginName": "kimmi_tm", + "words": [ + "kimmi_tm" + ] + }, { "color": "System", "flags": "Text", @@ -199,7 +220,7 @@ "trailingSpace": true, "type": "TextElement", "words": [ - "kimmi_tm!" + "!" ] }, { diff --git a/tests/snapshots/IrcMessageHandler/sub-multi-month-gift.json b/tests/snapshots/IrcMessageHandler/sub-multi-month-gift.json index 65246a879..94eead088 100644 --- a/tests/snapshots/IrcMessageHandler/sub-multi-month-gift.json +++ b/tests/snapshots/IrcMessageHandler/sub-multi-month-gift.json @@ -38,8 +38,9 @@ "type": "TimestampElement" }, { - "color": "System", - "flags": "Text", + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", "link": { "type": "None", "value": "" @@ -47,7 +48,9 @@ "style": "ChatMedium", "tooltip": "", "trailingSpace": true, - "type": "TextElement", + "type": "MentionElement", + "userColor": "#ffeb078d", + "userLoginName": "lucidfoxx", "words": [ "Lucidfoxx" ] @@ -187,6 +190,24 @@ "to" ] }, + { + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": false, + "type": "MentionElement", + "userColor": "System", + "userLoginName": "ogprodigy", + "words": [ + "OGprodigy" + ] + }, { "color": "System", "flags": "Text", @@ -199,7 +220,7 @@ "trailingSpace": true, "type": "TextElement", "words": [ - "OGprodigy!" + "!" ] } ], diff --git a/tests/snapshots/IrcMessageHandler/sub-multi-month-resub.json b/tests/snapshots/IrcMessageHandler/sub-multi-month-resub.json index 4878d3f4e..ffa926d54 100644 --- a/tests/snapshots/IrcMessageHandler/sub-multi-month-resub.json +++ b/tests/snapshots/IrcMessageHandler/sub-multi-month-resub.json @@ -38,8 +38,9 @@ "type": "TimestampElement" }, { - "color": "System", - "flags": "Text", + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", "link": { "type": "None", "value": "" @@ -47,7 +48,9 @@ "style": "ChatMedium", "tooltip": "", "trailingSpace": true, - "type": "TextElement", + "type": "MentionElement", + "userColor": "#ff0000ff", + "userLoginName": "calm__like_a_tom", "words": [ "calm__like_a_tom" ] diff --git a/tests/snapshots/IrcMessageHandler/sub-multi-month.json b/tests/snapshots/IrcMessageHandler/sub-multi-month.json index 98f7e34a4..4adf9f17a 100644 --- a/tests/snapshots/IrcMessageHandler/sub-multi-month.json +++ b/tests/snapshots/IrcMessageHandler/sub-multi-month.json @@ -38,8 +38,9 @@ "type": "TimestampElement" }, { - "color": "System", - "flags": "Text", + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", "link": { "type": "None", "value": "" @@ -47,7 +48,9 @@ "style": "ChatMedium", "tooltip": "", "trailingSpace": true, - "type": "TextElement", + "type": "MentionElement", + "userColor": "System", + "userLoginName": "foly__", "words": [ "foly__" ] diff --git a/tests/snapshots/IrcMessageHandler/sub-no-message.json b/tests/snapshots/IrcMessageHandler/sub-no-message.json index 01d589b90..7b866c3ae 100644 --- a/tests/snapshots/IrcMessageHandler/sub-no-message.json +++ b/tests/snapshots/IrcMessageHandler/sub-no-message.json @@ -38,8 +38,9 @@ "type": "TimestampElement" }, { - "color": "System", - "flags": "Text", + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", "link": { "type": "None", "value": "" @@ -47,7 +48,9 @@ "style": "ChatMedium", "tooltip": "", "trailingSpace": true, - "type": "TextElement", + "type": "MentionElement", + "userColor": "#ffcc00c2", + "userLoginName": "cspice", "words": [ "cspice" ] @@ -158,8 +161,9 @@ ] }, { - "color": "System", - "flags": "Text", + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", "link": { "type": "None", "value": "" @@ -167,7 +171,9 @@ "style": "ChatMedium", "tooltip": "", "trailingSpace": true, - "type": "TextElement", + "type": "MentionElement", + "userColor": "#ffcc00c2", + "userLoginName": "cspice", "words": [ "cspice" ]