diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3565dd6820f..3351e1d3f43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -169,7 +169,7 @@ jobs: - name: Setup sccache (Windows) # sccache v0.7.4 - uses: hendrikmuhs/ccache-action@v1.2.13 + uses: hendrikmuhs/ccache-action@v1.2.14 if: startsWith(matrix.os, 'windows') with: variant: sccache diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index 83269411c2f..4322c5b32c6 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -26,7 +26,7 @@ jobs: run: sudo apt-get -y install dos2unix - name: Check formatting - uses: DoozyX/clang-format-lint-action@v0.17 + uses: DoozyX/clang-format-lint-action@v0.18 with: source: "./src ./tests/src ./benchmarks/src ./mocks/include" extensions: "hpp,cpp" diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 973538d4ccd..8d97fe2aeb7 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -69,7 +69,7 @@ jobs: - name: Setup sccache # sccache v0.7.4 - uses: hendrikmuhs/ccache-action@v1.2.13 + uses: hendrikmuhs/ccache-action@v1.2.14 with: variant: sccache # only save on the default (master) branch diff --git a/CHANGELOG.md b/CHANGELOG.md index f458677cc18..c5a7c0744ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Minor: Replying to a message will now display the message being replied to. (#4350, #5519) - 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) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) - 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) @@ -32,6 +33,7 @@ - Bugfix: Fixed a crash when tab completing while having an invalid plugin loaded. (#5401) - Bugfix: Fixed windows on Windows not saving correctly when snapping them to the edges. (#5478) - Bugfix: Fixed user info card popups adding duplicate line to log files. (#5499) +- Bugfix: Fixed tooltips and input completion popups not working after moving a split. (#5541) - Bugfix: Fixed `/clearmessages` not working with more than one window. (#5489) - Bugfix: Fixed splits staying paused after unfocusing Chatterino in certain configurations. (#5504) - Bugfix: Links with invalid characters in the domain are no longer detected. (#5509) @@ -55,10 +57,16 @@ - Dev: Refactor/unsingletonize `UserDataController`. (#5459) - Dev: Cleanup `BrowserExtension`. (#5465) - Dev: Deprecate Qt 5.12. (#5396) +- Dev: Refactored `MessageFlag` into its own file. (#5549) - Dev: The running Qt version is now shown in the about page if it differs from the compiled version. (#5501) - Dev: `FlagsEnum` is now `constexpr`. (#5510) - Dev: Documented and added tests to RTL handling. (#5473) +- Dev: Refactored 7TV/BTTV definitions out of `TwitchIrcServer` into `Application`. (#5532) +- Dev: Refactored code that's responsible for deleting old update files. (#5535) +- Dev: Cleanly exit on shutdown. (#5537) +- Dev: Renamed threads created by Chatterino on Linux and Windows. (#5538, #5539, #5544) - Dev: Refactored a few `#define`s into `const(expr)` and cleaned includes. (#5527) +- Dev: Added `FlagsEnum::isEmpty`. (#5550) - Dev: Prepared for Qt 6.8 by addressing some deprecations. (#5529) ## 2.5.1 diff --git a/lib/expected-lite b/lib/expected-lite index 5d3c8d38fb2..f339d2f7373 160000 --- a/lib/expected-lite +++ b/lib/expected-lite @@ -1 +1 @@ -Subproject commit 5d3c8d38fb2bc0ccfc410f9d20033701057c400b +Subproject commit f339d2f73730f8fee4412f5e4938717866ecef48 diff --git a/mocks/include/mocks/BaseApplication.hpp b/mocks/include/mocks/BaseApplication.hpp index 720d1f68470..b858a87a1b9 100644 --- a/mocks/include/mocks/BaseApplication.hpp +++ b/mocks/include/mocks/BaseApplication.hpp @@ -2,6 +2,7 @@ #include "mocks/DisabledStreamerMode.hpp" #include "mocks/EmptyApplication.hpp" +#include "providers/bttv/BttvLiveUpdates.hpp" #include "singletons/Settings.hpp" #include @@ -30,6 +31,16 @@ class BaseApplication : public EmptyApplication return &this->streamerMode; } + BttvLiveUpdates *getBttvLiveUpdates() override + { + return nullptr; + } + + SeventvEventAPI *getSeventvEventAPI() override + { + return nullptr; + } + Settings settings; DisabledStreamerMode streamerMode; }; diff --git a/mocks/include/mocks/DisabledStreamerMode.hpp b/mocks/include/mocks/DisabledStreamerMode.hpp index 96c03b2b20b..747fdd0ade9 100644 --- a/mocks/include/mocks/DisabledStreamerMode.hpp +++ b/mocks/include/mocks/DisabledStreamerMode.hpp @@ -9,4 +9,8 @@ class DisabledStreamerMode : public chatterino::IStreamerMode { return false; } + + void start() override + { + } }; diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp index 89dea7828f8..94aabe5e7b0 100644 --- a/mocks/include/mocks/EmptyApplication.hpp +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -133,7 +133,7 @@ class EmptyApplication : public IApplication return nullptr; } - IAbstractIrcServer *getTwitchAbstract() override + ITwitchIrcServer *getTwitchAbstract() override { assert(false && "EmptyApplication::getTwitchAbstract was called " "without being initialized"); @@ -245,6 +245,13 @@ class EmptyApplication : public IApplication return nullptr; } + BttvLiveUpdates *getBttvLiveUpdates() override + { + assert(false && "EmptyApplication::getBttvLiveUpdates was called " + "without being initialized"); + return nullptr; + } + FfzEmotes *getFfzEmotes() override { assert(false && "EmptyApplication::getFfzEmotes was called without " @@ -259,6 +266,13 @@ class EmptyApplication : public IApplication return nullptr; } + SeventvEventAPI *getSeventvEventAPI() override + { + assert(false && "EmptyApplication::getSeventvEventAPI was called " + "without being initialized"); + return nullptr; + } + ILinkResolver *getLinkResolver() override { assert(false && "EmptyApplication::getLinkResolver was called without " diff --git a/mocks/include/mocks/TwitchIrcServer.hpp b/mocks/include/mocks/TwitchIrcServer.hpp index 42abc30ec5b..bbeba8ca47b 100644 --- a/mocks/include/mocks/TwitchIrcServer.hpp +++ b/mocks/include/mocks/TwitchIrcServer.hpp @@ -2,13 +2,11 @@ #include "mocks/Channel.hpp" #include "providers/bttv/BttvEmotes.hpp" -#include "providers/bttv/BttvLiveUpdates.hpp" #include "providers/ffz/FfzEmotes.hpp" #include "providers/seventv/eventapi/Client.hpp" #include "providers/seventv/eventapi/Dispatch.hpp" #include "providers/seventv/eventapi/Message.hpp" #include "providers/seventv/SeventvEmotes.hpp" -#include "providers/seventv/SeventvEventAPI.hpp" #include "providers/twitch/TwitchIrcServer.hpp" namespace chatterino::mock { @@ -28,6 +26,38 @@ class MockTwitchIrcServer : public ITwitchIrcServer { } + void connect() override + { + } + + void sendRawMessage(const QString &rawMessage) override + { + } + + ChannelPtr getOrAddChannel(const QString &dirtyChannelName) override + { + assert(false && "unimplemented getOrAddChannel in mock irc server"); + return {}; + } + + ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) override + { + assert(false && "unimplemented getChannelOrEmpty in mock irc server"); + return {}; + } + + void addFakeMessage(const QString &data) override + { + } + + void addGlobalSystemMessage(const QString &messageText) override + { + } + + void forEachChannel(std::function func) override + { + } + void forEachChannelAndSpecialChannels( std::function func) override { @@ -46,16 +76,6 @@ class MockTwitchIrcServer : public ITwitchIrcServer // } - std::unique_ptr &getBTTVLiveUpdates() override - { - return this->bttvLiveUpdates; - } - - std::unique_ptr &getSeventvEventAPI() override - { - return this->seventvEventAPI; - } - const IndirectChannel &getWatchingChannel() const override { return this->watchingChannel; @@ -103,9 +123,6 @@ class MockTwitchIrcServer : public ITwitchIrcServer ChannelPtr liveChannel; ChannelPtr automodChannel; QString lastUserThatWhisperedMe{"forsen"}; - - std::unique_ptr bttvLiveUpdates; - std::unique_ptr seventvEventAPI; }; } // namespace chatterino::mock diff --git a/src/Application.cpp b/src/Application.cpp index 32a177c99eb..5e69e817d65 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -14,7 +14,6 @@ #include "controllers/sound/ISoundController.hpp" #include "providers/bttv/BttvEmotes.hpp" #include "providers/ffz/FfzEmotes.hpp" -#include "providers/irc/AbstractIrcServer.hpp" #include "providers/links/LinkResolver.hpp" #include "providers/seventv/SeventvAPI.hpp" #include "providers/seventv/SeventvEmotes.hpp" @@ -34,7 +33,6 @@ #include "providers/bttv/BttvLiveUpdates.hpp" #include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/ffz/FfzBadges.hpp" -#include "providers/irc/Irc2.hpp" #include "providers/seventv/eventapi/Dispatch.hpp" #include "providers/seventv/eventapi/Subscription.hpp" #include "providers/seventv/SeventvBadges.hpp" @@ -75,6 +73,9 @@ namespace { using namespace chatterino; +const QString BTTV_LIVE_UPDATES_URL = "wss://sockets.betterttv.net/ws"; +const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3"; + ISoundController *makeSoundController(Settings &settings) { SoundBackend soundBackend = settings.soundBackend; @@ -97,20 +98,50 @@ ISoundController *makeSoundController(Settings &settings) } } +BttvLiveUpdates *makeBttvLiveUpdates(Settings &settings) +{ + bool enabled = + settings.enableBTTVLiveUpdates && settings.enableBTTVChannelEmotes; + + if (enabled) + { + return new BttvLiveUpdates(BTTV_LIVE_UPDATES_URL); + } + + return nullptr; +} + +SeventvEventAPI *makeSeventvEventAPI(Settings &settings) +{ + bool enabled = settings.enableSevenTVEventAPI; + + if (enabled) + { + return new SeventvEventAPI(SEVENTV_EVENTAPI_URL); + } + + return nullptr; +} + const QString TWITCH_PUBSUB_URL = "wss://pubsub-edge.twitch.tv"; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +IApplication *INSTANCE = nullptr; + } // namespace namespace chatterino { static std::atomic isAppInitialized{false}; -Application *Application::instance = nullptr; -IApplication *IApplication::instance = nullptr; - IApplication::IApplication() { - IApplication::instance = this; + INSTANCE = this; +} + +IApplication::~IApplication() +{ + INSTANCE = nullptr; } // this class is responsible for handling the workflow of Chatterino @@ -147,8 +178,10 @@ Application::Application(Settings &_settings, const Paths &paths, , twitchBadges(new TwitchBadges) , chatterinoBadges(new ChatterinoBadges) , bttvEmotes(new BttvEmotes) + , bttvLiveUpdates(makeBttvLiveUpdates(_settings)) , ffzEmotes(new FfzEmotes) , seventvEmotes(new SeventvEmotes) + , seventvEventAPI(makeSeventvEventAPI(_settings)) , logging(new Logging(_settings)) , linkResolver(new LinkResolver) , streamerMode(new StreamerMode) @@ -157,8 +190,6 @@ Application::Application(Settings &_settings, const Paths &paths, #endif , updates(_updates) { - Application::instance = this; - // We can safely ignore this signal's connection since the Application will always // be destroyed after fonts std::ignore = this->fonts->fontChanged.connect([this]() { @@ -166,39 +197,10 @@ Application::Application(Settings &_settings, const Paths &paths, }); } -Application::~Application() = default; - -void Application::fakeDtor() +Application::~Application() { -#ifdef CHATTERINO_HAVE_PLUGINS - this->plugins.reset(); -#endif - this->twitchPubSub.reset(); - this->twitchBadges.reset(); - this->twitchLiveController.reset(); - this->chatterinoBadges.reset(); - this->bttvEmotes.reset(); - this->ffzEmotes.reset(); - this->seventvEmotes.reset(); - this->notifications.reset(); - this->commands.reset(); - // If a crash happens after crashHandler has been reset, we'll assert - // This isn't super different from before, where if the app is already killed, the getApp() portion of it is already dead - this->crashHandler.reset(); - this->seventvAPI.reset(); - this->highlights.reset(); - this->seventvBadges.reset(); - this->ffzBadges.reset(); - // this->twitch.reset(); - this->imageUploader.reset(); - this->hotkeys.reset(); - this->fonts.reset(); - this->sound.reset(); - this->userData.reset(); - this->toasts.reset(); - this->accounts.reset(); - this->emotes.reset(); - this->themes.reset(); + // we do this early to ensure getApp isn't used in any dtors + INSTANCE = nullptr; } void Application::initialize(Settings &settings, const Paths &paths) @@ -261,11 +263,6 @@ void Application::initialize(Settings &settings, const Paths &paths) if (!this->args_.isFramelessEmbed) { getSettings()->currentVersion.setValue(CHATTERINO_VERSION); - - if (getSettings()->enableExperimentalIrc) - { - Irc::instance().load(); - } } this->accounts->load(); @@ -321,6 +318,8 @@ void Application::initialize(Settings &settings, const Paths &paths) this->initBttvLiveUpdates(); this->initSeventvEventAPI(); + + this->streamerMode->start(); } int Application::run(QApplication &qtApp) @@ -567,6 +566,13 @@ PluginController *Application::getPlugins() } #endif +Updates &Application::getUpdates() +{ + assertInGuiThread(); + + return this->updates; +} + ITwitchIrcServer *Application::getTwitch() { assertInGuiThread(); @@ -574,7 +580,7 @@ ITwitchIrcServer *Application::getTwitch() return this->twitch.get(); } -IAbstractIrcServer *Application::getTwitchAbstract() +ITwitchIrcServer *Application::getTwitchAbstract() { assertInGuiThread(); @@ -615,6 +621,14 @@ BttvEmotes *Application::getBttvEmotes() return this->bttvEmotes.get(); } +BttvLiveUpdates *Application::getBttvLiveUpdates() +{ + assertInGuiThread(); + // bttvLiveUpdates may be nullptr if it's not enabled + + return this->bttvLiveUpdates.get(); +} + FfzEmotes *Application::getFfzEmotes() { assertInGuiThread(); @@ -645,6 +659,14 @@ SeventvPaints *Application::getSeventvPaints() return this->seventvPaints.get(); } +SeventvEventAPI *Application::getSeventvEventAPI() +{ + assertInGuiThread(); + // seventvEventAPI may be nullptr if it's not enabled + + return this->seventvEventAPI.get(); +} + void Application::save() { this->commands->save(); @@ -1121,9 +1143,7 @@ void Application::initPubSub() void Application::initBttvLiveUpdates() { - auto &bttvLiveUpdates = this->twitch->getBTTVLiveUpdates(); - - if (!bttvLiveUpdates) + if (!this->bttvLiveUpdates) { qCDebug(chatterinoBttv) << "Skipping initialization of Live Updates as it's disabled"; @@ -1132,8 +1152,8 @@ void Application::initBttvLiveUpdates() // We can safely ignore these signal connections since the twitch object will always // be destroyed before the Application - std::ignore = - bttvLiveUpdates->signals_.emoteAdded.connect([&](const auto &data) { + std::ignore = this->bttvLiveUpdates->signals_.emoteAdded.connect( + [&](const auto &data) { auto chan = this->twitch->getChannelOrEmptyByID(data.channelID); postToThread([chan, data] { @@ -1143,8 +1163,8 @@ void Application::initBttvLiveUpdates() } }); }); - std::ignore = - bttvLiveUpdates->signals_.emoteUpdated.connect([&](const auto &data) { + std::ignore = this->bttvLiveUpdates->signals_.emoteUpdated.connect( + [&](const auto &data) { auto chan = this->twitch->getChannelOrEmptyByID(data.channelID); postToThread([chan, data] { @@ -1154,8 +1174,8 @@ void Application::initBttvLiveUpdates() } }); }); - std::ignore = - bttvLiveUpdates->signals_.emoteRemoved.connect([&](const auto &data) { + std::ignore = this->bttvLiveUpdates->signals_.emoteRemoved.connect( + [&](const auto &data) { auto chan = this->twitch->getChannelOrEmptyByID(data.channelID); postToThread([chan, data] { @@ -1165,14 +1185,12 @@ void Application::initBttvLiveUpdates() } }); }); - bttvLiveUpdates->start(); + this->bttvLiveUpdates->start(); } void Application::initSeventvEventAPI() { - auto &seventvEventAPI = this->twitch->getSeventvEventAPI(); - - if (!seventvEventAPI) + if (!this->seventvEventAPI) { qCDebug(chatterinoSeventvEventAPI) << "Skipping initialization as the EventAPI is disabled"; @@ -1181,8 +1199,8 @@ void Application::initSeventvEventAPI() // We can safely ignore these signal connections since the twitch object will always // be destroyed before the Application - std::ignore = - seventvEventAPI->signals_.emoteAdded.connect([&](const auto &data) { + std::ignore = this->seventvEventAPI->signals_.emoteAdded.connect( + [&](const auto &data) { if (this->seventvPersonalEmotes->hasEmoteSet(data.emoteSetID)) { this->seventvPersonalEmotes->updateEmoteSet(data.emoteSetID, @@ -1198,8 +1216,8 @@ void Application::initSeventvEventAPI() }); } }); - std::ignore = - seventvEventAPI->signals_.emoteUpdated.connect([&](const auto &data) { + std::ignore = this->seventvEventAPI->signals_.emoteUpdated.connect( + [&](const auto &data) { if (this->seventvPersonalEmotes->hasEmoteSet(data.emoteSetID)) { this->seventvPersonalEmotes->updateEmoteSet(data.emoteSetID, @@ -1215,8 +1233,8 @@ void Application::initSeventvEventAPI() }); } }); - std::ignore = - seventvEventAPI->signals_.emoteRemoved.connect([&](const auto &data) { + std::ignore = this->seventvEventAPI->signals_.emoteRemoved.connect( + [&](const auto &data) { if (this->seventvPersonalEmotes->hasEmoteSet(data.emoteSetID)) { this->seventvPersonalEmotes->updateEmoteSet(data.emoteSetID, @@ -1232,8 +1250,8 @@ void Application::initSeventvEventAPI() }); } }); - std::ignore = - seventvEventAPI->signals_.userUpdated.connect([&](const auto &data) { + std::ignore = this->seventvEventAPI->signals_.userUpdated.connect( + [&](const auto &data) { this->twitch->forEachSeventvUser(data.userID, [data](TwitchChannel &chan) { chan.updateSeventvUser(data); @@ -1253,14 +1271,14 @@ void Application::initSeventvEventAPI() }); }); - seventvEventAPI->start(); + this->seventvEventAPI->start(); } IApplication *getApp() { - assert(IApplication::instance != nullptr); + assert(INSTANCE != nullptr); - return IApplication::instance; + return INSTANCE; } } // namespace chatterino diff --git a/src/Application.hpp b/src/Application.hpp index 47aea0300cf..bf9f0da045f 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -1,6 +1,5 @@ #pragma once -#include "debug/AssertInGuiThread.hpp" #include "singletons/NativeMessaging.hpp" #include @@ -52,19 +51,23 @@ class ImageUploader; class SeventvAPI; class CrashHandler; class BttvEmotes; +class BttvLiveUpdates; class FfzEmotes; class SeventvEmotes; +class SeventvEventAPI; class ILinkResolver; class IStreamerMode; -class IAbstractIrcServer; class IApplication { public: IApplication(); - virtual ~IApplication() = default; + virtual ~IApplication(); - static IApplication *instance; + IApplication(const IApplication &) = delete; + IApplication(IApplication &&) = delete; + IApplication &operator=(const IApplication &) = delete; + IApplication &operator=(IApplication &&) = delete; virtual bool isTest() const = 0; @@ -82,7 +85,7 @@ class IApplication virtual HighlightController *getHighlights() = 0; virtual NotificationController *getNotifications() = 0; virtual ITwitchIrcServer *getTwitch() = 0; - virtual IAbstractIrcServer *getTwitchAbstract() = 0; + virtual ITwitchIrcServer *getTwitchAbstract() = 0; virtual PubSub *getTwitchPubSub() = 0; virtual ILogging *getChatLogger() = 0; virtual IChatterinoBadges *getChatterinoBadges() = 0; @@ -102,8 +105,10 @@ class IApplication #endif virtual Updates &getUpdates() = 0; virtual BttvEmotes *getBttvEmotes() = 0; + virtual BttvLiveUpdates *getBttvLiveUpdates() = 0; virtual FfzEmotes *getFfzEmotes() = 0; virtual SeventvEmotes *getSeventvEmotes() = 0; + virtual SeventvEventAPI *getSeventvEventAPI() = 0; virtual ILinkResolver *getLinkResolver() = 0; virtual IStreamerMode *getStreamerMode() = 0; }; @@ -116,8 +121,6 @@ class Application : public IApplication char **argv_{}; public: - static Application *instance; - Application(Settings &_settings, const Paths &paths, const Args &_args, Updates &_updates); ~Application() override; @@ -132,12 +135,6 @@ class Application : public IApplication return false; } - /** - * In the interim, before we remove _exit(0); from RunGui.cpp, - * this will destroy things we know can be destroyed - */ - void fakeDtor(); - void initialize(Settings &settings, const Paths &paths); void load(); void save(); @@ -172,8 +169,10 @@ class Application : public IApplication std::unique_ptr twitchBadges; std::unique_ptr chatterinoBadges; std::unique_ptr bttvEmotes; + std::unique_ptr bttvLiveUpdates; std::unique_ptr ffzEmotes; std::unique_ptr seventvEmotes; + std::unique_ptr seventvEventAPI; const std::unique_ptr logging; std::unique_ptr linkResolver; std::unique_ptr streamerMode; @@ -202,7 +201,8 @@ class Application : public IApplication NotificationController *getNotifications() override; HighlightController *getHighlights() override; ITwitchIrcServer *getTwitch() override; - IAbstractIrcServer *getTwitchAbstract() override; + [[deprecated("use getTwitch()")]] ITwitchIrcServer *getTwitchAbstract() + override; PubSub *getTwitchPubSub() override; ILogging *getChatLogger() override; FfzBadges *getFfzBadges() override; @@ -217,19 +217,16 @@ class Application : public IApplication #ifdef CHATTERINO_HAVE_PLUGINS PluginController *getPlugins() override; #endif - Updates &getUpdates() override - { - assertInGuiThread(); - - return this->updates; - } + Updates &getUpdates() override; SeventvPersonalEmotes *getSeventvPersonalEmotes() override; SeventvPaints *getSeventvPaints() override; BttvEmotes *getBttvEmotes() override; + BttvLiveUpdates *getBttvLiveUpdates() override; FfzEmotes *getFfzEmotes() override; SeventvEmotes *getSeventvEmotes() override; + SeventvEventAPI *getSeventvEventAPI() override; ILinkResolver *getLinkResolver() override; IStreamerMode *getStreamerMode() override; @@ -240,7 +237,7 @@ class Application : public IApplication void initSeventvEventAPI(); void initNm(const Paths &paths); - NativeMessagingServer nmServer{}; + NativeMessagingServer nmServer; Updates &updates; }; diff --git a/src/BrowserExtension.cpp b/src/BrowserExtension.cpp index 8511cbcb078..4b0f7e69a02 100644 --- a/src/BrowserExtension.cpp +++ b/src/BrowserExtension.cpp @@ -1,6 +1,7 @@ #include "BrowserExtension.hpp" #include "singletons/NativeMessaging.hpp" +#include "util/RenameThread.hpp" #include #include @@ -69,6 +70,7 @@ void runLoop() std::this_thread::sleep_for(10s); } }); + renameThread(thread, "BrowserPingCheck"); while (true) { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3b4b1648b8a..8c263c53fc0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -276,6 +276,7 @@ set(SOURCE_FILES messages/MessageColor.hpp messages/MessageElement.cpp messages/MessageElement.hpp + messages/MessageFlag.hpp messages/MessageThread.cpp messages/MessageThread.hpp @@ -355,22 +356,8 @@ set(SOURCE_FILES providers/ffz/FfzUtil.cpp providers/ffz/FfzUtil.hpp - providers/irc/AbstractIrcServer.cpp - providers/irc/AbstractIrcServer.hpp - providers/irc/Irc2.cpp - providers/irc/Irc2.hpp - providers/irc/IrcAccount.cpp - providers/irc/IrcAccount.hpp - providers/irc/IrcChannel2.cpp - providers/irc/IrcChannel2.hpp - providers/irc/IrcCommands.cpp - providers/irc/IrcCommands.hpp providers/irc/IrcConnection2.cpp providers/irc/IrcConnection2.hpp - providers/irc/IrcMessageBuilder.cpp - providers/irc/IrcMessageBuilder.hpp - providers/irc/IrcServer.cpp - providers/irc/IrcServer.hpp providers/links/LinkInfo.cpp providers/links/LinkInfo.hpp @@ -530,6 +517,8 @@ set(SOURCE_FILES util/RapidjsonHelpers.hpp util/RatelimitBucket.cpp util/RatelimitBucket.hpp + util/RenameThread.cpp + util/RenameThread.hpp util/SampleData.cpp util/SampleData.hpp util/SharedPtrElementLess.hpp @@ -592,9 +581,6 @@ set(SOURCE_FILES widgets/dialogs/EditHotkeyDialog.hpp widgets/dialogs/EmotePopup.cpp widgets/dialogs/EmotePopup.hpp - widgets/dialogs/IrcConnectionEditor.cpp - widgets/dialogs/IrcConnectionEditor.hpp - widgets/dialogs/IrcConnectionEditor.ui widgets/dialogs/LastRunCrashDialog.cpp widgets/dialogs/LastRunCrashDialog.hpp widgets/dialogs/LoginDialog.cpp diff --git a/src/RunGui.cpp b/src/RunGui.cpp index 06e9a7b9446..4863f4da7dc 100644 --- a/src/RunGui.cpp +++ b/src/RunGui.cpp @@ -241,22 +241,7 @@ void runGui(QApplication &a, const Paths &paths, Settings &settings, } #endif - auto thread = std::thread([dir = paths.miscDirectory] { - { - auto path = combinePath(dir, "Update.exe"); - if (QFile::exists(path)) - { - QFile::remove(path); - } - } - { - auto path = combinePath(dir, "update.zip"); - if (QFile::exists(path)) - { - QFile::remove(path); - } - } - }); + updates.deleteOldFiles(); // Clear the cache 1 minute after start. QTimer::singleShot(60 * 1000, [cachePath = paths.cacheDirectory(), @@ -292,9 +277,6 @@ void runGui(QApplication &a, const Paths &paths, Settings &settings, // flushing windows clipboard to keep copied messages flushClipboard(); #endif - - app.fakeDtor(); - - _exit(0); } + } // namespace chatterino diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index bbf58f0a7f7..b3372615187 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -3,8 +3,6 @@ #include "Application.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcServer.hpp" #include "providers/twitch/IrcMessageHandler.hpp" #include "singletons/Emotes.hpp" #include "singletons/Logging.hpp" @@ -26,7 +24,7 @@ namespace chatterino { // Channel // Channel::Channel(const QString &name, Type type) - : completionModel(*this, nullptr) + : completionModel(new TabCompletionModel(*this, nullptr)) , lastDate_(QDate::currentDate()) , name_(name) , messages_(getSettings()->scrollbackSplitLimit) @@ -36,8 +34,6 @@ Channel::Channel(const QString &name, Type type) { this->platform_ = "twitch"; } - - // Irc platform is set through IrcChannel2 ctor } Channel::~Channel() diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index b85fb83cbc3..67e715af5d1 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -1,8 +1,8 @@ #pragma once -#include "common/FlagsEnum.hpp" #include "controllers/completion/TabCompletionModel.hpp" #include "messages/LimitedQueue.hpp" +#include "messages/MessageFlag.hpp" #include #include @@ -17,8 +17,6 @@ namespace chatterino { struct Message; using MessagePtr = std::shared_ptr; -enum class MessageFlag : int64_t; -using MessageFlags = FlagsEnum; enum class TimeoutStackStyle : int { StackHard = 0, @@ -53,7 +51,6 @@ class Channel : public std::enable_shared_from_this TwitchLive, TwitchAutomod, TwitchEnd, - Irc, Misc, }; @@ -123,7 +120,7 @@ class Channel : public std::enable_shared_from_this static std::shared_ptr getEmpty(); - TabCompletionModel completionModel; + TabCompletionModel *completionModel; QDate lastDate_; protected: @@ -185,8 +182,6 @@ constexpr magic_enum::customize::customize_t return "live"; case Type::TwitchAutomod: return "automod"; - case Type::Irc: - return "irc"; case Type::Misc: return "misc"; default: diff --git a/src/common/FlagsEnum.hpp b/src/common/FlagsEnum.hpp index e49c30aa63a..d2ae4ebf7c1 100644 --- a/src/common/FlagsEnum.hpp +++ b/src/common/FlagsEnum.hpp @@ -132,6 +132,12 @@ class FlagsEnum return this->hasNone(FlagsEnum{flags...}); } + /// Returns true if the enum has no flag set (i.e. its underlying value is 0) + constexpr bool isEmpty() const noexcept + { + return static_cast(this->value_) == 0; + } + constexpr T value() const noexcept { return this->value_; diff --git a/src/common/IrcColors.hpp b/src/common/IrcColors.hpp deleted file mode 100644 index 9721105650b..00000000000 --- a/src/common/IrcColors.hpp +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include -#include - -namespace chatterino { - -// Colors taken from https://modern.ircdocs.horse/formatting.html -static QMap IRC_COLORS = { - {0, QColor("white")}, {1, QColor("black")}, - {2, QColor("blue")}, {3, QColor("green")}, - {4, QColor("red")}, {5, QColor("brown")}, - {6, QColor("purple")}, {7, QColor("orange")}, - {8, QColor("yellow")}, {9, QColor("lightgreen")}, - {10, QColor("cyan")}, {11, QColor("lightcyan")}, - {12, QColor("lightblue")}, {13, QColor("pink")}, - {14, QColor("gray")}, {15, QColor("lightgray")}, - {16, QColor("#470000")}, {17, QColor("#472100")}, - {18, QColor("#474700")}, {19, QColor("#324700")}, - {20, QColor("#004700")}, {21, QColor("#00472c")}, - {22, QColor("#004747")}, {23, QColor("#002747")}, - {24, QColor("#000047")}, {25, QColor("#2e0047")}, - {26, QColor("#470047")}, {27, QColor("#47002a")}, - {28, QColor("#740000")}, {29, QColor("#743a00")}, - {30, QColor("#747400")}, {31, QColor("#517400")}, - {32, QColor("#007400")}, {33, QColor("#007449")}, - {34, QColor("#007474")}, {35, QColor("#004074")}, - {36, QColor("#000074")}, {37, QColor("#4b0074")}, - {38, QColor("#740074")}, {39, QColor("#740045")}, - {40, QColor("#b50000")}, {41, QColor("#b56300")}, - {42, QColor("#b5b500")}, {43, QColor("#7db500")}, - {44, QColor("#00b500")}, {45, QColor("#00b571")}, - {46, QColor("#00b5b5")}, {47, QColor("#0063b5")}, - {48, QColor("#0000b5")}, {49, QColor("#7500b5")}, - {50, QColor("#b500b5")}, {51, QColor("#b5006b")}, - {52, QColor("#ff0000")}, {53, QColor("#ff8c00")}, - {54, QColor("#ffff00")}, {55, QColor("#b2ff00")}, - {56, QColor("#00ff00")}, {57, QColor("#00ffa0")}, - {58, QColor("#00ffff")}, {59, QColor("#008cff")}, - {60, QColor("#0000ff")}, {61, QColor("#a500ff")}, - {62, QColor("#ff00ff")}, {63, QColor("#ff0098")}, - {64, QColor("#ff5959")}, {65, QColor("#ffb459")}, - {66, QColor("#ffff71")}, {67, QColor("#cfff60")}, - {68, QColor("#6fff6f")}, {69, QColor("#65ffc9")}, - {70, QColor("#6dffff")}, {71, QColor("#59b4ff")}, - {72, QColor("#5959ff")}, {73, QColor("#c459ff")}, - {74, QColor("#ff66ff")}, {75, QColor("#ff59bc")}, - {76, QColor("#ff9c9c")}, {77, QColor("#ffd39c")}, - {78, QColor("#ffff9c")}, {79, QColor("#e2ff9c")}, - {80, QColor("#9cff9c")}, {81, QColor("#9cffdb")}, - {82, QColor("#9cffff")}, {83, QColor("#9cd3ff")}, - {84, QColor("#9c9cff")}, {85, QColor("#dc9cff")}, - {86, QColor("#ff9cff")}, {87, QColor("#ff94d3")}, - {88, QColor("#000000")}, {89, QColor("#131313")}, - {90, QColor("#282828")}, {91, QColor("#363636")}, - {92, QColor("#4d4d4d")}, {93, QColor("#656565")}, - {94, QColor("#818181")}, {95, QColor("#9f9f9f")}, - {96, QColor("#bcbcbc")}, {97, QColor("#e2e2e2")}, - {98, QColor("#ffffff")}, -}; - -} // namespace chatterino diff --git a/src/common/network/NetworkManager.cpp b/src/common/network/NetworkManager.cpp index eb1b7ec5229..b1b6bd28f1e 100644 --- a/src/common/network/NetworkManager.cpp +++ b/src/common/network/NetworkManager.cpp @@ -13,6 +13,7 @@ void NetworkManager::init() assert(!NetworkManager::accessManager); NetworkManager::workerThread = new QThread; + NetworkManager::workerThread->setObjectName("NetworkWorker"); NetworkManager::workerThread->start(); NetworkManager::accessManager = new QNetworkAccessManager; diff --git a/src/controllers/commands/builtin/twitch/SendWhisper.cpp b/src/controllers/commands/builtin/twitch/SendWhisper.cpp index 3a33ee973e1..cb965d5793d 100644 --- a/src/controllers/commands/builtin/twitch/SendWhisper.cpp +++ b/src/controllers/commands/builtin/twitch/SendWhisper.cpp @@ -9,8 +9,6 @@ #include "messages/MessageElement.hpp" #include "providers/bttv/BttvEmotes.hpp" #include "providers/ffz/FfzEmotes.hpp" -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcServer.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchIrcServer.hpp" @@ -242,17 +240,6 @@ QString sendWhisper(const CommandContext &ctx) return ""; } - // we must be on IRC - auto *ircChannel = dynamic_cast(ctx.channel.get()); - if (ircChannel == nullptr) - { - // give up - return ""; - } - - auto *server = ircChannel->server(); - server->sendWhisper(target, message); - return ""; } diff --git a/src/controllers/highlights/HighlightController.hpp b/src/controllers/highlights/HighlightController.hpp index c217f7bb208..488f628fb31 100644 --- a/src/controllers/highlights/HighlightController.hpp +++ b/src/controllers/highlights/HighlightController.hpp @@ -1,7 +1,7 @@ #pragma once -#include "common/FlagsEnum.hpp" #include "common/UniqueAccess.hpp" +#include "messages/MessageFlag.hpp" #include "singletons/Settings.hpp" #include @@ -19,8 +19,6 @@ namespace chatterino { class Badge; struct MessageParseArgs; -enum class MessageFlag : int64_t; -using MessageFlags = FlagsEnum; class AccountController; struct HighlightResult { diff --git a/src/controllers/sound/MiniaudioBackend.cpp b/src/controllers/sound/MiniaudioBackend.cpp index 63b7efcf0c5..3a88bbf3069 100644 --- a/src/controllers/sound/MiniaudioBackend.cpp +++ b/src/controllers/sound/MiniaudioBackend.cpp @@ -6,6 +6,7 @@ #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" +#include "util/RenameThread.hpp" #include "widgets/Window.hpp" #include @@ -193,6 +194,7 @@ MiniaudioBackend::MiniaudioBackend() this->audioThread = std::make_unique([this] { this->ioContext.run(); }); + renameThread(*this->audioThread, "C2Miniaudio"); } MiniaudioBackend::~MiniaudioBackend() diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index 16372e9c35d..c06dd947e8a 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -1,10 +1,9 @@ #pragma once -#include "common/FlagsEnum.hpp" +#include "messages/MessageFlag.hpp" #include "providers/twitch/ChannelPointReward.hpp" #include "util/QStringHash.hpp" -#include #include #include @@ -20,53 +19,6 @@ class MessageThread; class Badge; class ScrollbarHighlight; -enum class MessageFlag : int64_t { - None = 0LL, - System = (1LL << 0), - Timeout = (1LL << 1), - Highlighted = (1LL << 2), - DoNotTriggerNotification = (1LL << 3), // disable notification sound - Centered = (1LL << 4), - Disabled = (1LL << 5), - DisableCompactEmotes = (1LL << 6), - Collapsed = (1LL << 7), - ConnectedMessage = (1LL << 8), - DisconnectedMessage = (1LL << 9), - Untimeout = (1LL << 10), - PubSub = (1LL << 11), - Subscription = (1LL << 12), - DoNotLog = (1LL << 13), - AutoMod = (1LL << 14), - RecentMessage = (1LL << 15), - Whisper = (1LL << 16), - HighlightedWhisper = (1LL << 17), - Debug = (1LL << 18), - Similar = (1LL << 19), - RedeemedHighlight = (1LL << 20), - RedeemedChannelPointReward = (1LL << 21), - ShowInMentions = (1LL << 22), - FirstMessage = (1LL << 23), - ReplyMessage = (1LL << 24), - ElevatedMessage = (1LL << 25), - SubscribedThread = (1LL << 26), - CheerMessage = (1LL << 27), - LiveUpdatesAdd = (1LL << 28), - LiveUpdatesRemove = (1LL << 29), - LiveUpdatesUpdate = (1LL << 30), - /// The header of a message caught by AutoMod containing allow/disallow - AutoModOffendingMessageHeader = (1LL << 31), - /// The message caught by AutoMod containing the user who sent the message & its contents - AutoModOffendingMessage = (1LL << 32), - LowTrustUsers = (1LL << 33), - /// The message is sent by a user marked as restricted with Twitch's "Low Trust"/"Suspicious User" feature - RestrictedMessage = (1LL << 34), - /// The message is sent by a user marked as monitor with Twitch's "Low Trust"/"Suspicious User" feature - MonitoredMessage = (1LL << 35), - /// The message is an ACTION message (/me) - Action = (1LL << 36), -}; -using MessageFlags = FlagsEnum; - struct Message; using MessagePtr = std::shared_ptr; struct Message { @@ -123,8 +75,3 @@ struct Message { }; } // namespace chatterino - -template <> -struct magic_enum::customize::enum_range { - static constexpr bool is_flags = true; -}; diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index ae78ab71120..29f4abb7fbb 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -1,10 +1,8 @@ #include "messages/MessageBuilder.hpp" #include "Application.hpp" -#include "common/IrcColors.hpp" #include "common/LinkParser.hpp" #include "controllers/accounts/AccountController.hpp" -#include "messages/Image.hpp" #include "messages/Message.hpp" #include "messages/MessageColor.hpp" #include "messages/MessageElement.hpp" @@ -12,18 +10,12 @@ #include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "singletons/Emotes.hpp" -#include "singletons/Resources.hpp" -#include "singletons/Theme.hpp" #include "util/FormatTime.hpp" #include namespace { -QRegularExpression IRC_COLOR_PARSE_REGEX( - "(\u0003(\\d{1,2})?(,(\\d{1,2}))?|\u000f)", - QRegularExpression::UseUnicodePropertiesOption); - QString formatUpdatedEmoteList(const QString &platform, const std::vector &emoteNames, bool isAdd, bool isFirstWord) @@ -679,107 +671,6 @@ void MessageBuilder::addLink(const linkparser::Parsed &parsedLink, getApp()->getLinkResolver()->resolve(el->linkInfo()); } -void MessageBuilder::addIrcMessageText(const QString &text) -{ - this->message().messageText = text; - - auto words = text.split(' '); - MessageColor defaultColorType = MessageColor::Text; - const auto &defaultColor = - defaultColorType.getColor(*getApp()->getThemes()); - QColor textColor = defaultColor; - int fg = -1; - int bg = -1; - - for (const auto &string : words) - { - if (string.isEmpty()) - { - continue; - } - - // Actually just text - auto link = linkparser::parse(string); - if (link) - { - this->addLink(*link, string); - continue; - } - - // Does the word contain a color changer? If so, split on it. - // Add color indicators, then combine into the same word with the color being changed - - auto i = IRC_COLOR_PARSE_REGEX.globalMatch(string); - - if (!i.hasNext()) - { - this->addIrcWord(string, textColor); - continue; - } - - int lastPos = 0; - - while (i.hasNext()) - { - auto match = i.next(); - - if (lastPos != match.capturedStart() && match.capturedStart() != 0) - { - if (fg >= 0 && fg <= 98) - { - textColor = IRC_COLORS[fg]; - getApp()->getThemes()->normalizeColor(textColor); - } - else - { - textColor = defaultColor; - } - this->addIrcWord( - string.mid(lastPos, match.capturedStart() - lastPos), - textColor, false); - lastPos = match.capturedStart() + match.capturedLength(); - } - if (!match.captured(1).isEmpty()) - { - fg = -1; - bg = -1; - } - - if (!match.captured(2).isEmpty()) - { - fg = match.captured(2).toInt(nullptr); - } - else - { - fg = -1; - } - if (!match.captured(4).isEmpty()) - { - bg = match.captured(4).toInt(nullptr); - } - else if (fg == -1) - { - bg = -1; - } - - lastPos = match.capturedStart() + match.capturedLength(); - } - - if (fg >= 0 && fg <= 98) - { - textColor = IRC_COLORS[fg]; - getApp()->getThemes()->normalizeColor(textColor); - } - else - { - textColor = defaultColor; - } - this->addIrcWord(string.mid(lastPos), textColor); - } - - this->message().elements.back()->setTrailingSpace(false); -} - void MessageBuilder::addTextOrEmoji(EmotePtr emote) { this->emplace(emote, MessageElementFlag::EmojiAll); @@ -806,24 +697,6 @@ void MessageBuilder::addTextOrEmoji(const QString &string) } } -void MessageBuilder::addIrcWord(const QString &text, const QColor &color, - bool addSpace) -{ - this->textColor_ = color; - for (auto &variant : getApp()->getEmotes()->getEmojis()->parse(text)) - { - boost::apply_visitor( - [&](auto &&arg) { - this->addTextOrEmoji(arg); - }, - variant); - if (!addSpace) - { - this->message().elements.back()->setTrailingSpace(false); - } - } -} - TextElement *MessageBuilder::emplaceSystemTextAndUpdate(const QString &text, QString &toUpdate) { diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 89de9921260..e9844467d07 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -116,13 +116,6 @@ class MessageBuilder void append(std::unique_ptr element); void addLink(const linkparser::Parsed &parsedLink, const QString &source); - /** - * Adds the text, applies irc colors, adds links, - * and updates the message's messageText. - * See https://modern.ircdocs.horse/formatting.html - */ - void addIrcMessageText(const QString &text); - template // clang-format off // clang-format can be enabled once clang-format v11+ has been installed in CI @@ -155,17 +148,6 @@ class MessageBuilder TextElement *emplaceSystemTextAndUpdate(const QString &text, QString &toUpdate); - /** - * This will add the text and replace any emojis - * with an emoji emote-element. - * - * @param text Text to add - * @param color Color of the text - * @param addSpace true if a trailing space should be added after emojis - */ - void addIrcWord(const QString &text, const QColor &color, - bool addSpace = true); - std::shared_ptr message_; }; diff --git a/src/messages/MessageFlag.hpp b/src/messages/MessageFlag.hpp new file mode 100644 index 00000000000..7648dadc793 --- /dev/null +++ b/src/messages/MessageFlag.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "common/FlagsEnum.hpp" + +#include + +namespace chatterino { + +enum class MessageFlag : std::int64_t { + None = 0LL, + System = (1LL << 0), + Timeout = (1LL << 1), + Highlighted = (1LL << 2), + DoNotTriggerNotification = (1LL << 3), // disable notification sound + Centered = (1LL << 4), + Disabled = (1LL << 5), + DisableCompactEmotes = (1LL << 6), + Collapsed = (1LL << 7), + ConnectedMessage = (1LL << 8), + DisconnectedMessage = (1LL << 9), + Untimeout = (1LL << 10), + PubSub = (1LL << 11), + Subscription = (1LL << 12), + DoNotLog = (1LL << 13), + AutoMod = (1LL << 14), + RecentMessage = (1LL << 15), + Whisper = (1LL << 16), + HighlightedWhisper = (1LL << 17), + Debug = (1LL << 18), + Similar = (1LL << 19), + RedeemedHighlight = (1LL << 20), + RedeemedChannelPointReward = (1LL << 21), + ShowInMentions = (1LL << 22), + FirstMessage = (1LL << 23), + ReplyMessage = (1LL << 24), + ElevatedMessage = (1LL << 25), + SubscribedThread = (1LL << 26), + CheerMessage = (1LL << 27), + LiveUpdatesAdd = (1LL << 28), + LiveUpdatesRemove = (1LL << 29), + LiveUpdatesUpdate = (1LL << 30), + /// The header of a message caught by AutoMod containing allow/disallow + AutoModOffendingMessageHeader = (1LL << 31), + /// The message caught by AutoMod containing the user who sent the message & its contents + AutoModOffendingMessage = (1LL << 32), + LowTrustUsers = (1LL << 33), + /// The message is sent by a user marked as restricted with Twitch's "Low Trust"/"Suspicious User" feature + RestrictedMessage = (1LL << 34), + /// The message is sent by a user marked as monitor with Twitch's "Low Trust"/"Suspicious User" feature + MonitoredMessage = (1LL << 35), + /// The message is an ACTION message (/me) + Action = (1LL << 36), +}; +using MessageFlags = FlagsEnum; + +} // namespace chatterino + +template <> +struct magic_enum::customize::enum_range { + // NOLINTNEXTLINE(readability-identifier-naming) + static constexpr bool is_flags = true; +}; diff --git a/src/messages/layouts/MessageLayoutContainer.hpp b/src/messages/layouts/MessageLayoutContainer.hpp index 7c14faf40af..1cd7f6af2c2 100644 --- a/src/messages/layouts/MessageLayoutContainer.hpp +++ b/src/messages/layouts/MessageLayoutContainer.hpp @@ -2,6 +2,7 @@ #include "common/Common.hpp" #include "common/FlagsEnum.hpp" +#include "messages/MessageFlag.hpp" #include #include @@ -24,8 +25,6 @@ enum class TextDirection : uint8_t { LTR, }; -enum class MessageFlag : int64_t; -using MessageFlags = FlagsEnum; class MessageLayoutElement; struct Selection; struct MessagePaintContext; diff --git a/src/providers/bttv/BttvLiveUpdates.cpp b/src/providers/bttv/BttvLiveUpdates.cpp index f9128b5c792..b2658937c9a 100644 --- a/src/providers/bttv/BttvLiveUpdates.cpp +++ b/src/providers/bttv/BttvLiveUpdates.cpp @@ -1,13 +1,17 @@ #include "providers/bttv/BttvLiveUpdates.hpp" +#include "common/Literals.hpp" + #include #include namespace chatterino { +using namespace chatterino::literals; + BttvLiveUpdates::BttvLiveUpdates(QString host) - : BasicPubSubManager(std::move(host)) + : BasicPubSubManager(std::move(host), u"BTTV"_s) { } diff --git a/src/providers/irc/AbstractIrcServer.cpp b/src/providers/irc/AbstractIrcServer.cpp deleted file mode 100644 index 599a8e0551c..00000000000 --- a/src/providers/irc/AbstractIrcServer.cpp +++ /dev/null @@ -1,435 +0,0 @@ -#include "providers/irc/AbstractIrcServer.hpp" - -#include "common/Channel.hpp" -#include "common/QLogging.hpp" -#include "messages/LimitedQueueSnapshot.hpp" -#include "messages/Message.hpp" -#include "messages/MessageBuilder.hpp" -#include "providers/twitch/TwitchChannel.hpp" - -#include - -namespace chatterino { - -// Ratelimits for joinBucket_ -const int JOIN_RATELIMIT_BUDGET = 18; -const int JOIN_RATELIMIT_COOLDOWN = 12500; - -AbstractIrcServer::AbstractIrcServer() -{ - // Initialize the connections - // XXX: don't create write connection if there is no separate write connection. - this->writeConnection_.reset(new IrcConnection); - this->writeConnection_->moveToThread( - QCoreApplication::instance()->thread()); - - // Apply a leaky bucket rate limiting to JOIN messages - auto actuallyJoin = [&](QString message) { - if (!this->channels.contains(message)) - { - return; - } - this->readConnection_->sendRaw("JOIN #" + message); - }; - this->joinBucket_.reset(new RatelimitBucket( - JOIN_RATELIMIT_BUDGET, JOIN_RATELIMIT_COOLDOWN, actuallyJoin, this)); - - QObject::connect(this->writeConnection_.get(), - &Communi::IrcConnection::messageReceived, this, - [this](auto msg) { - this->writeConnectionMessageReceived(msg); - }); - QObject::connect(this->writeConnection_.get(), - &Communi::IrcConnection::connected, this, [this] { - this->onWriteConnected(this->writeConnection_.get()); - }); - this->connections_.managedConnect( - this->writeConnection_->connectionLost, [this](bool timeout) { - qCDebug(chatterinoIrc) - << "Write connection reconnect requested. Timeout:" << timeout; - this->writeConnection_->smartReconnect(); - }); - - // Listen to read connection message signals - this->readConnection_.reset(new IrcConnection); - this->readConnection_->moveToThread(QCoreApplication::instance()->thread()); - - QObject::connect(this->readConnection_.get(), - &Communi::IrcConnection::messageReceived, this, - [this](auto msg) { - this->readConnectionMessageReceived(msg); - }); - QObject::connect(this->readConnection_.get(), - &Communi::IrcConnection::privateMessageReceived, this, - [this](auto msg) { - this->privateMessageReceived(msg); - }); - QObject::connect(this->readConnection_.get(), - &Communi::IrcConnection::connected, this, [this] { - this->onReadConnected(this->readConnection_.get()); - }); - QObject::connect(this->readConnection_.get(), - &Communi::IrcConnection::disconnected, this, [this] { - this->onDisconnected(); - }); - this->connections_.managedConnect( - this->readConnection_->connectionLost, [this](bool timeout) { - qCDebug(chatterinoIrc) - << "Read connection reconnect requested. Timeout:" << timeout; - if (timeout) - { - // Show additional message since this is going to interrupt a - // connection that is still "connected" - this->addGlobalSystemMessage( - "Server connection timed out, reconnecting"); - } - this->readConnection_->smartReconnect(); - }); - this->connections_.managedConnect(this->readConnection_->heartbeat, [this] { - this->markChannelsConnected(); - }); -} - -void AbstractIrcServer::initializeIrc() -{ - assert(!this->initialized_); - - if (this->hasSeparateWriteConnection()) - { - this->initializeConnectionSignals(this->writeConnection_.get(), - ConnectionType::Write); - this->initializeConnectionSignals(this->readConnection_.get(), - ConnectionType::Read); - } - else - { - this->initializeConnectionSignals(this->readConnection_.get(), - ConnectionType::Both); - } - - this->initialized_ = true; -} - -void AbstractIrcServer::connect() -{ - assert(this->initialized_); - - this->disconnect(); - - if (this->hasSeparateWriteConnection()) - { - this->initializeConnection(this->writeConnection_.get(), Write); - this->initializeConnection(this->readConnection_.get(), Read); - } - else - { - this->initializeConnection(this->readConnection_.get(), Both); - } -} - -void AbstractIrcServer::open(ConnectionType type) -{ - std::lock_guard lock(this->connectionMutex_); - - if (type == Write) - { - this->writeConnection_->open(); - } - if (type & Read) - { - this->readConnection_->open(); - } -} - -void AbstractIrcServer::addGlobalSystemMessage(const QString &messageText) -{ - std::lock_guard lock(this->channelMutex); - - MessageBuilder b(systemMessage, messageText); - auto message = b.release(); - - for (std::weak_ptr &weak : this->channels.values()) - { - std::shared_ptr chan = weak.lock(); - if (!chan) - { - continue; - } - - chan->addMessage(message, MessageContext::Original); - } -} - -void AbstractIrcServer::disconnect() -{ - std::lock_guard locker(this->connectionMutex_); - - this->readConnection_->close(); - if (this->hasSeparateWriteConnection()) - { - this->writeConnection_->close(); - } -} - -void AbstractIrcServer::sendMessage(const QString &channelName, - const QString &message) -{ - this->sendRawMessage("PRIVMSG #" + channelName + " :" + message); -} - -void AbstractIrcServer::sendRawMessage(const QString &rawMessage) -{ - std::lock_guard locker(this->connectionMutex_); - - if (this->hasSeparateWriteConnection()) - { - this->writeConnection_->sendRaw(rawMessage); - } - else - { - this->readConnection_->sendRaw(rawMessage); - } -} - -void AbstractIrcServer::writeConnectionMessageReceived( - Communi::IrcMessage *message) -{ - (void)message; -} - -ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName) -{ - auto channelName = this->cleanChannelName(dirtyChannelName); - - // try get channel - ChannelPtr chan = this->getChannelOrEmpty(channelName); - if (chan != Channel::getEmpty()) - { - return chan; - } - - std::lock_guard lock(this->channelMutex); - - // value doesn't exist - chan = this->createChannel(channelName); - if (!chan) - { - return Channel::getEmpty(); - } - - this->channels.insert(channelName, chan); - this->connections_.managedConnect(chan->destroyed, [this, channelName] { - // fourtf: issues when the server itself is destroyed - - qCDebug(chatterinoIrc) << "[AbstractIrcServer::addChannel]" - << channelName << "was destroyed"; - this->channels.remove(channelName); - - if (this->readConnection_) - { - this->readConnection_->sendRaw("PART #" + channelName); - } - }); - - // join IRC channel - { - std::lock_guard lock2(this->connectionMutex_); - - if (this->readConnection_) - { - if (this->readConnection_->isConnected()) - { - this->joinBucket_->send(channelName); - } - } - } - - return chan; -} - -ChannelPtr AbstractIrcServer::getChannelOrEmpty(const QString &dirtyChannelName) -{ - auto channelName = this->cleanChannelName(dirtyChannelName); - - std::lock_guard lock(this->channelMutex); - - // try get special channel - ChannelPtr chan = this->getCustomChannel(channelName); - if (chan) - { - return chan; - } - - // value exists - auto it = this->channels.find(channelName); - if (it != this->channels.end()) - { - chan = it.value().lock(); - - if (chan) - { - return chan; - } - } - - return Channel::getEmpty(); -} - -std::vector> AbstractIrcServer::getChannels() -{ - std::lock_guard lock(this->channelMutex); - std::vector> channels; - - for (auto &&weak : this->channels.values()) - { - channels.push_back(weak); - } - - return channels; -} - -void AbstractIrcServer::onReadConnected(IrcConnection *connection) -{ - (void)connection; - - std::lock_guard lock(this->channelMutex); - - // join channels - for (auto &&weak : this->channels) - { - if (auto channel = weak.lock()) - { - this->joinBucket_->send(channel->getName()); - } - } - - // connected/disconnected message - auto connectedMsg = makeSystemMessage("connected"); - connectedMsg->flags.set(MessageFlag::ConnectedMessage); - auto reconnected = makeSystemMessage("reconnected"); - reconnected->flags.set(MessageFlag::ConnectedMessage); - - for (std::weak_ptr &weak : this->channels.values()) - { - std::shared_ptr chan = weak.lock(); - if (!chan) - { - continue; - } - - LimitedQueueSnapshot snapshot = chan->getMessageSnapshot(); - - bool replaceMessage = - snapshot.size() > 0 && snapshot[snapshot.size() - 1]->flags.has( - MessageFlag::DisconnectedMessage); - - if (replaceMessage) - { - chan->replaceMessage(snapshot[snapshot.size() - 1], reconnected); - } - else - { - chan->addMessage(connectedMsg, MessageContext::Original); - } - } - - this->falloffCounter_ = 1; -} - -void AbstractIrcServer::onWriteConnected(IrcConnection *connection) -{ - (void)connection; -} - -void AbstractIrcServer::onDisconnected() -{ - std::lock_guard lock(this->channelMutex); - - MessageBuilder b(systemMessage, "disconnected"); - b->flags.set(MessageFlag::DisconnectedMessage); - auto disconnectedMsg = b.release(); - - for (std::weak_ptr &weak : this->channels.values()) - { - std::shared_ptr chan = weak.lock(); - if (!chan) - { - continue; - } - - chan->addMessage(disconnectedMsg, MessageContext::Original); - - if (auto *channel = dynamic_cast(chan.get())) - { - channel->markDisconnected(); - } - } -} - -void AbstractIrcServer::markChannelsConnected() -{ - this->forEachChannel([](const ChannelPtr &chan) { - if (auto *channel = dynamic_cast(chan.get())) - { - channel->markConnected(); - } - }); -} - -std::shared_ptr AbstractIrcServer::getCustomChannel( - const QString &channelName) -{ - (void)channelName; - return nullptr; -} - -QString AbstractIrcServer::cleanChannelName(const QString &dirtyChannelName) -{ - // This function is a Noop only for IRC, for Twitch it removes a leading '#' and lowercases the name - return dirtyChannelName; -} - -void AbstractIrcServer::addFakeMessage(const QString &data) -{ - auto *fakeMessage = Communi::IrcMessage::fromData( - data.toUtf8(), this->readConnection_.get()); - - if (fakeMessage->command() == "PRIVMSG") - { - this->privateMessageReceived( - static_cast(fakeMessage)); - } - else - { - this->readConnectionMessageReceived(fakeMessage); - } -} - -void AbstractIrcServer::privateMessageReceived( - Communi::IrcPrivateMessage *message) -{ - (void)message; -} - -void AbstractIrcServer::readConnectionMessageReceived( - Communi::IrcMessage *message) -{ -} - -void AbstractIrcServer::forEachChannel(std::function func) -{ - std::lock_guard lock(this->channelMutex); - - for (std::weak_ptr &weak : this->channels.values()) - { - ChannelPtr chan = weak.lock(); - if (!chan) - { - continue; - } - - func(chan); - } -} - -} // namespace chatterino diff --git a/src/providers/irc/AbstractIrcServer.hpp b/src/providers/irc/AbstractIrcServer.hpp deleted file mode 100644 index 0b626f9e0be..00000000000 --- a/src/providers/irc/AbstractIrcServer.hpp +++ /dev/null @@ -1,135 +0,0 @@ -#pragma once - -#include "common/Common.hpp" -#include "providers/irc/IrcConnection2.hpp" -#include "util/RatelimitBucket.hpp" - -#include -#include -#include - -#include -#include - -namespace chatterino { - -class Channel; -using ChannelPtr = std::shared_ptr; -class RatelimitBucket; - -class IAbstractIrcServer -{ -public: - virtual void connect() = 0; - - virtual void sendRawMessage(const QString &rawMessage) = 0; - - virtual ChannelPtr getOrAddChannel(const QString &dirtyChannelName) = 0; - virtual ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) = 0; - - virtual void addFakeMessage(const QString &data) = 0; - - virtual void addGlobalSystemMessage(const QString &messageText) = 0; - - virtual void forEachChannel(std::function func) = 0; -}; - -class AbstractIrcServer : public IAbstractIrcServer, public QObject -{ -public: - enum ConnectionType { Read = 1, Write = 2, Both = 3 }; - - ~AbstractIrcServer() override = default; - AbstractIrcServer(const AbstractIrcServer &) = delete; - AbstractIrcServer(AbstractIrcServer &&) = delete; - AbstractIrcServer &operator=(const AbstractIrcServer &) = delete; - AbstractIrcServer &operator=(AbstractIrcServer &&) = delete; - - // initializeIrc must be called from the derived class - // this allows us to initialize the abstract IRC server based on the derived class's parameters - void initializeIrc(); - - // connection - void connect() final; - void disconnect(); - - void sendMessage(const QString &channelName, const QString &message); - void sendRawMessage(const QString &rawMessage) override; - - // channels - ChannelPtr getOrAddChannel(const QString &dirtyChannelName) final; - ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) final; - std::vector> getChannels(); - - // signals - pajlada::Signals::NoArgSignal connected; - pajlada::Signals::NoArgSignal disconnected; - - void addFakeMessage(const QString &data) final; - - void addGlobalSystemMessage(const QString &messageText) final; - - // iteration - void forEachChannel(std::function func) final; - -protected: - AbstractIrcServer(); - - // initializeConnectionSignals is called on a connection once in its lifetime. - // it can be used to connect signals to your class - virtual void initializeConnectionSignals(IrcConnection *connection, - ConnectionType type) - { - (void)connection; - (void)type; - } - - // initializeConnection is called every time before we try to connect to the IRC server - virtual void initializeConnection(IrcConnection *connection, - ConnectionType type) = 0; - - virtual std::shared_ptr createChannel( - const QString &channelName) = 0; - - virtual void privateMessageReceived(Communi::IrcPrivateMessage *message); - virtual void readConnectionMessageReceived(Communi::IrcMessage *message); - virtual void writeConnectionMessageReceived(Communi::IrcMessage *message); - - virtual void onReadConnected(IrcConnection *connection); - virtual void onWriteConnected(IrcConnection *connection); - virtual void onDisconnected(); - void markChannelsConnected(); - - virtual std::shared_ptr getCustomChannel( - const QString &channelName); - - virtual bool hasSeparateWriteConnection() const = 0; - virtual QString cleanChannelName(const QString &dirtyChannelName); - - void open(ConnectionType type); - - QMap> channels; - std::mutex channelMutex; - -private: - void initConnection(); - - QObjectPtr writeConnection_ = nullptr; - QObjectPtr readConnection_ = nullptr; - - // Our rate limiting bucket for the Twitch join rate limits - // https://dev.twitch.tv/docs/irc/guide#rate-limits - QObjectPtr joinBucket_; - - QTimer reconnectTimer_; - int falloffCounter_ = 1; - - std::mutex connectionMutex_; - - // bool autoReconnect_ = false; - pajlada::Signals::SignalHolder connections_; - - bool initialized_{false}; -}; - -} // namespace chatterino diff --git a/src/providers/irc/Irc2.cpp b/src/providers/irc/Irc2.cpp deleted file mode 100644 index 48b3cd0a130..00000000000 --- a/src/providers/irc/Irc2.cpp +++ /dev/null @@ -1,289 +0,0 @@ -#include "providers/irc/Irc2.hpp" - -#include "Application.hpp" -#include "common/Credentials.hpp" -#include "common/SignalVectorModel.hpp" -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcServer.hpp" -#include "singletons/Paths.hpp" -#include "util/CombinePath.hpp" -#include "util/RapidjsonHelpers.hpp" -#include "util/StandardItemHelper.hpp" - -#include -#include -#include - -#include - -namespace chatterino { - -namespace { - - QString configPath() - { - return combinePath(getApp()->getPaths().settingsDirectory, "irc.json"); - } - - class Model : public SignalVectorModel - { - public: - Model(QObject *parent) - : SignalVectorModel(6, parent) - { - } - - // turn a vector item into a model row - IrcServerData getItemFromRow(std::vector &row, - const IrcServerData &original) override - { - return IrcServerData{ - row[0]->data(Qt::EditRole).toString(), // host - row[1]->data(Qt::EditRole).toInt(), // port - row[2]->data(Qt::CheckStateRole).toBool(), // ssl - row[3]->data(Qt::EditRole).toString(), // user - row[4]->data(Qt::EditRole).toString(), // nick - row[5]->data(Qt::EditRole).toString(), // real - original.authType, // authType - original.connectCommands, // connectCommands - original.id, // id - }; - } - - // turns a row in the model into a vector item - void getRowFromItem(const IrcServerData &item, - std::vector &row) override - { - setStringItem(row[0], item.host, false); - setStringItem(row[1], QString::number(item.port)); - setBoolItem(row[2], item.ssl); - setStringItem(row[3], item.user); - setStringItem(row[4], item.nick); - setStringItem(row[5], item.real); - } - }; - -} // namespace - -inline QString escape(QString str) -{ - return str.replace(":", "::"); -} - -// This returns a unique id for every server which is understandeable in the systems credential manager. -inline QString getCredentialName(const IrcServerData &data) -{ - return escape(QString::number(data.id)) + ":" + escape(data.user) + "@" + - escape(data.host); -} - -void IrcServerData::getPassword( - QObject *receiver, std::function &&onLoaded) const -{ - Credentials::instance().get("irc", getCredentialName(*this), receiver, - std::move(onLoaded)); -} - -void IrcServerData::setPassword(const QString &password) -{ - Credentials::instance().set("irc", getCredentialName(*this), password); -} - -Irc::Irc() -{ - // We can safely ignore this signal connection since `connections` will always - // be destroyed before the Irc object - std::ignore = this->connections.itemInserted.connect([this](auto &&args) { - // make sure only one id can only exist for one server - assert(this->servers_.find(args.item.id) == this->servers_.end()); - - // add new server - if (auto ab = this->abandonedChannels_.find(args.item.id); - ab != this->abandonedChannels_.end()) - { - auto server = std::make_unique(args.item, ab->second); - - // set server of abandoned channels - for (auto weak : ab->second) - { - if (auto shared = weak.lock()) - { - if (auto *ircChannel = - dynamic_cast(shared.get())) - { - ircChannel->setServer(server.get()); - } - } - } - - // add new server with abandoned channels - this->servers_.emplace(args.item.id, std::move(server)); - this->abandonedChannels_.erase(ab); - } - else - { - // add new server - this->servers_.emplace(args.item.id, - std::make_unique(args.item)); - } - }); - - // We can safely ignore this signal connection since `connections` will always - // be destroyed before the Irc object - std::ignore = this->connections.itemRemoved.connect([this](auto &&args) { - // restore - if (auto server = this->servers_.find(args.item.id); - server != this->servers_.end()) - { - auto abandoned = server->second->getChannels(); - - // set server of abandoned servers to nullptr - for (auto weak : abandoned) - { - if (auto shared = weak.lock()) - { - if (auto *ircChannel = - dynamic_cast(shared.get())) - { - ircChannel->setServer(nullptr); - } - } - } - - this->abandonedChannels_[args.item.id] = abandoned; - this->servers_.erase(server); - } - - if (args.caller != Irc::noEraseCredentialCaller) - { - Credentials::instance().erase("irc", getCredentialName(args.item)); - } - }); - - // We can safely ignore this signal connection since `connections` will always - // be destroyed before the Irc object - std::ignore = this->connections.delayedItemsChanged.connect([this] { - this->save(); - }); -} - -QAbstractTableModel *Irc::newConnectionModel(QObject *parent) -{ - auto *model = new Model(parent); - model->initialize(&this->connections); - return model; -} - -ChannelPtr Irc::getOrAddChannel(int id, QString name) -{ - if (auto server = this->servers_.find(id); server != this->servers_.end()) - { - return server->second->getOrAddChannel(name); - } - else - { - auto channel = std::make_shared(name, nullptr); - - this->abandonedChannels_[id].push_back(channel); - - return std::move(channel); - } -} - -Irc &Irc::instance() -{ - static Irc irc; - return irc; -} - -int Irc::uniqueId() -{ - int i = this->currentId_ + 1; - auto it = this->servers_.find(i); - auto it2 = this->abandonedChannels_.find(i); - - while (it != this->servers_.end() || it2 != this->abandonedChannels_.end()) - { - i++; - it = this->servers_.find(i); - it2 = this->abandonedChannels_.find(i); - } - - return (this->currentId_ = i); -} - -void Irc::save() -{ - QJsonDocument doc; - QJsonObject root; - QJsonArray servers; - - for (auto &&conn : this->connections) - { - QJsonObject obj; - obj.insert("host", conn.host); - obj.insert("port", conn.port); - obj.insert("ssl", conn.ssl); - obj.insert("username", conn.user); - obj.insert("nickname", conn.nick); - obj.insert("realname", conn.real); - obj.insert("connectCommands", - QJsonArray::fromStringList(conn.connectCommands)); - obj.insert("id", conn.id); - obj.insert("authType", int(conn.authType)); - - servers.append(obj); - } - - root.insert("servers", servers); - doc.setObject(root); - - QSaveFile file(configPath()); - file.open(QIODevice::WriteOnly); - file.write(doc.toJson()); - file.commit(); -} - -void Irc::load() -{ - if (this->loaded_) - { - return; - } - this->loaded_ = true; - - QString config = configPath(); - QFile file(configPath()); - file.open(QIODevice::ReadOnly); - auto object = QJsonDocument::fromJson(file.readAll()).object(); - - std::unordered_set ids; - - // load servers - for (auto server : object.value("servers").toArray()) - { - auto obj = server.toObject(); - IrcServerData data; - data.host = obj.value("host").toString(data.host); - data.port = obj.value("port").toInt(data.port); - data.ssl = obj.value("ssl").toBool(data.ssl); - data.user = obj.value("username").toString(data.user); - data.nick = obj.value("nickname").toString(data.nick); - data.real = obj.value("realname").toString(data.real); - data.connectCommands = - obj.value("connectCommands").toVariant().toStringList(); - data.id = obj.value("id").toInt(data.id); - data.authType = - IrcAuthType(obj.value("authType").toInt(int(data.authType))); - - // duplicate id's are not allowed :( - if (ids.find(data.id) == ids.end()) - { - ids.insert(data.id); - - this->connections.append(data); - } - } -} - -} // namespace chatterino diff --git a/src/providers/irc/Irc2.hpp b/src/providers/irc/Irc2.hpp deleted file mode 100644 index 915fbc6b953..00000000000 --- a/src/providers/irc/Irc2.hpp +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include "common/SignalVector.hpp" - -#include - -#include - -class QAbstractTableModel; - -namespace chatterino { - -class Channel; -using ChannelPtr = std::shared_ptr; -class IrcServer; - -enum class IrcAuthType { Anonymous, Custom, Pass, Sasl }; - -struct IrcServerData { - QString host; - int port = 6697; - bool ssl = true; - - QString user; - QString nick; - QString real; - - IrcAuthType authType = IrcAuthType::Anonymous; - void getPassword(QObject *receiver, - std::function &&onLoaded) const; - void setPassword(const QString &password); - - QStringList connectCommands; - - int id; -}; - -class Irc -{ -public: - Irc(); - - static Irc &instance(); - - static inline void *const noEraseCredentialCaller = - reinterpret_cast(1); - - SignalVector connections; - QAbstractTableModel *newConnectionModel(QObject *parent); - - ChannelPtr getOrAddChannel(int serverId, QString name); - - void save(); - void load(); - - int uniqueId(); - -private: - int currentId_{}; - bool loaded_{}; - - // Servers have a unique id. - // When a server gets changed it gets removed and then added again. - // So we store the channels of that server in abandonedChannels_ temporarily. - // Or if the server got removed permanently then it's still stored there. - std::unordered_map> servers_; - std::unordered_map>> - abandonedChannels_; -}; - -} // namespace chatterino diff --git a/src/providers/irc/IrcAccount.cpp b/src/providers/irc/IrcAccount.cpp deleted file mode 100644 index 53030252154..00000000000 --- a/src/providers/irc/IrcAccount.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "providers/irc/IrcAccount.hpp" - -// namespace chatterino { -// -// IrcAccount::IrcAccount(const QString &_userName, const QString &_nickName, -// const QString -// &_realName, -// const QString &_password) -// : userName(_userName) -// , nickName(_nickName) -// , realName(_realName) -// , password(_password) -//{ -//} - -// const QString &IrcAccount::getUserName() const -//{ -// return this->userName; -//} - -// const QString &IrcAccount::getNickName() const -//{ -// return this->nickName; -//} - -// const QString &IrcAccount::getRealName() const -//{ -// return this->realName; -//} - -// const QString &IrcAccount::getPassword() const -//{ -// return this->password; -//} -// -//} // namespace chatterino diff --git a/src/providers/irc/IrcAccount.hpp b/src/providers/irc/IrcAccount.hpp deleted file mode 100644 index 2c4345b1040..00000000000 --- a/src/providers/irc/IrcAccount.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include - -// namespace chatterino { -// -// class IrcAccount -//{ -// public: -// IrcAccount(const QString &userName, const QString &nickName, const QString -// &realName, -// const QString &password); - -// const QString &getUserName() const; -// const QString &getNickName() const; -// const QString &getRealName() const; -// const QString &getPassword() const; - -// private: -// QString userName; -// QString nickName; -// QString realName; -// QString password; -//}; -// -//} // namespace chatterino diff --git a/src/providers/irc/IrcChannel2.cpp b/src/providers/irc/IrcChannel2.cpp deleted file mode 100644 index 76feb901b16..00000000000 --- a/src/providers/irc/IrcChannel2.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "providers/irc/IrcChannel2.hpp" - -#include "common/Channel.hpp" -#include "debug/AssertInGuiThread.hpp" -#include "messages/Message.hpp" -#include "messages/MessageBuilder.hpp" -#include "messages/MessageElement.hpp" -#include "providers/irc/IrcCommands.hpp" -#include "providers/irc/IrcMessageBuilder.hpp" -#include "providers/irc/IrcServer.hpp" -#include "util/Helpers.hpp" - -namespace chatterino { - -IrcChannel::IrcChannel(const QString &name, IrcServer *server) - : Channel(name, Channel::Type::Irc) - , ChannelChatters(*static_cast(this)) - , server_(server) -{ - auto *ircServer = this->server(); - if (ircServer != nullptr) - { - this->platform_ = - QString("irc-%1").arg(ircServer->userFriendlyIdentifier()); - } - else - { - this->platform_ = "irc-unknown"; - } -} - -void IrcChannel::sendMessage(const QString &message) -{ - assertInGuiThread(); - if (message.isEmpty()) - { - return; - } - - if (message.startsWith("/")) - { - auto index = message.indexOf(' ', 1); - QString command = message.mid(1, index - 1); - QString params = index == -1 ? "" : message.mid(index + 1); - - invokeIrcCommand(command, params, *this); - } - else - { - if (this->server() != nullptr) - { - this->server()->sendMessage(this->getName(), message); - if (this->server()->hasEcho()) - { - return; - } - MessageBuilder builder; - - builder - .emplace("#" + this->getName(), - MessageElementFlag::ChannelName, - MessageColor::System) - ->setLink({Link::JumpToChannel, this->getName()}); - - auto now = QDateTime::currentDateTime(); - builder.emplace(now.time()); - builder.message().serverReceivedTime = now; - - auto username = this->server()->nick(); - builder - .emplace( - username + ":", MessageElementFlag::Username, - getRandomColor(username), FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, username}); - builder.message().loginName = username; - builder.message().displayName = username; - - // message - builder.addIrcMessageText(message); - builder.message().messageText = message; - builder.message().searchText = username + ": " + message; - - this->addMessage(builder.release(), MessageContext::Original); - } - else - { - this->addSystemMessage("You are not connected."); - } - } -} - -IrcServer *IrcChannel::server() const -{ - assertInGuiThread(); - - return this->server_; -} - -void IrcChannel::setServer(IrcServer *server) -{ - assertInGuiThread(); - - this->server_ = server; -} - -bool IrcChannel::canReconnect() const -{ - return true; -} - -void IrcChannel::reconnect() -{ - if (this->server()) - { - this->server()->connect(); - } -} - -} // namespace chatterino diff --git a/src/providers/irc/IrcChannel2.hpp b/src/providers/irc/IrcChannel2.hpp deleted file mode 100644 index 75ff40ea473..00000000000 --- a/src/providers/irc/IrcChannel2.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "common/Channel.hpp" -#include "common/ChannelChatters.hpp" - -namespace chatterino { - -class Irc; -class IrcServer; - -class IrcChannel final : public Channel, public ChannelChatters -{ -public: - explicit IrcChannel(const QString &name, IrcServer *server); - - void sendMessage(const QString &message) override; - - // server may be nullptr - IrcServer *server() const; - - // Channel methods - bool canReconnect() const override; - void reconnect() override; - -private: - void setServer(IrcServer *server); - - IrcServer *server_; - - friend class Irc; -}; - -} // namespace chatterino diff --git a/src/providers/irc/IrcCommands.cpp b/src/providers/irc/IrcCommands.cpp deleted file mode 100644 index b1b2e8fc3c2..00000000000 --- a/src/providers/irc/IrcCommands.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "providers/irc/IrcCommands.hpp" - -#include "messages/MessageBuilder.hpp" -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcServer.hpp" -#include "util/QStringHash.hpp" - -namespace chatterino { - -Outcome invokeIrcCommand(const QString &commandName, const QString &allParams, - IrcChannel &channel) -{ - if (!channel.server()) - { - return Failure; - } - - // STATIC MESSAGES - static auto staticMessages = std::unordered_map{ - {"join", "/join is not supported. Press ctrl+r to change the " - "channel. If required use /raw JOIN #channel."}, - {"part", "/part is not supported. Press ctrl+r to change the " - "channel. If required use /raw PART #channel."}, - }; - auto cmd = commandName.toLower(); - - if (auto it = staticMessages.find(cmd); it != staticMessages.end()) - { - channel.addSystemMessage(it->second); - return Success; - } - - // CUSTOM COMMANDS - auto params = allParams.split(' '); - auto paramsAfter = [&](int i) { - return params.mid(i + 1).join(' '); - }; - - auto sendRaw = [&](QString str) { - channel.server()->sendRawMessage(str); - }; - - if (cmd == "msg") - { - channel.server()->sendWhisper(params[0], paramsAfter(0)); - } - else if (cmd == "away") - { - sendRaw("AWAY " + params[0] + " :" + paramsAfter(0)); - } - else if (cmd == "knock") - { - sendRaw("KNOCK #" + params[0] + " " + paramsAfter(0)); - } - else if (cmd == "kick") - { - if (params.size() < 2) - { - channel.addSystemMessage( - "Usage: /kick [message]"); - return Failure; - } - const auto &channelParam = params[0]; - const auto &clientParam = params[1]; - const auto &messageParam = paramsAfter(1); - if (messageParam.isEmpty()) - { - sendRaw("KICK " + channelParam + " " + clientParam); - } - else - { - sendRaw("KICK " + channelParam + " " + clientParam + " :" + - messageParam); - } - } - else if (cmd == "wallops") - { - sendRaw("WALLOPS :" + allParams); - } - else if (cmd == "raw") - { - sendRaw(allParams); - } - else - { - sendRaw(cmd.toUpper() + " " + allParams); - } - - return Success; -} - -} // namespace chatterino diff --git a/src/providers/irc/IrcCommands.hpp b/src/providers/irc/IrcCommands.hpp deleted file mode 100644 index 38f7db173db..00000000000 --- a/src/providers/irc/IrcCommands.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include "common/Outcome.hpp" - -#include - -namespace chatterino { - -class IrcChannel; - -Outcome invokeIrcCommand(const QString &command, const QString ¶ms, - IrcChannel &channel); - -} // namespace chatterino diff --git a/src/providers/irc/IrcMessageBuilder.cpp b/src/providers/irc/IrcMessageBuilder.cpp deleted file mode 100644 index 3b7fb30da25..00000000000 --- a/src/providers/irc/IrcMessageBuilder.cpp +++ /dev/null @@ -1,147 +0,0 @@ -#include "providers/irc/IrcMessageBuilder.hpp" - -#include "controllers/ignores/IgnoreController.hpp" -#include "controllers/ignores/IgnorePhrase.hpp" -#include "messages/Message.hpp" -#include "messages/MessageColor.hpp" -#include "messages/MessageElement.hpp" -#include "singletons/Emotes.hpp" -#include "singletons/Settings.hpp" -#include "singletons/Theme.hpp" -#include "singletons/WindowManager.hpp" -#include "util/Helpers.hpp" -#include "util/IrcHelpers.hpp" -#include "widgets/Window.hpp" - -namespace chatterino { - -IrcMessageBuilder::IrcMessageBuilder( - Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args) - : SharedMessageBuilder(_channel, _ircMessage, _args) -{ -} - -IrcMessageBuilder::IrcMessageBuilder(Channel *_channel, - const Communi::IrcMessage *_ircMessage, - const MessageParseArgs &_args, - QString content, bool isAction) - : SharedMessageBuilder(_channel, _ircMessage, _args, content, isAction) -{ - assert(false); -} - -IrcMessageBuilder::IrcMessageBuilder( - const Communi::IrcNoticeMessage *_ircMessage, const MessageParseArgs &_args) - : SharedMessageBuilder(Channel::getEmpty().get(), _ircMessage, _args, - _ircMessage->content(), false) -{ -} - -IrcMessageBuilder::IrcMessageBuilder( - const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args) - : SharedMessageBuilder(Channel::getEmpty().get(), _ircMessage, _args, - _ircMessage->content(), false) - , whisperTarget_(_ircMessage->target()) -{ -} - -MessagePtr IrcMessageBuilder::build() -{ - // PARSE - this->parse(); - this->usernameColor_ = getRandomColor(this->ircMessage->nick()); - - // PUSH ELEMENTS - this->appendChannelName(); - - this->message().serverReceivedTime = calculateMessageTime(this->ircMessage); - this->emplace(this->message().serverReceivedTime.time()); - - this->appendUsername(); - - // message - this->addIrcMessageText(this->originalMessage_); - - QString stylizedUsername = - this->stylizeUsername(this->userName, this->message()); - - this->message().searchText = stylizedUsername + " " + - this->message().localizedName + " " + - this->userName + ": " + this->originalMessage_; - - // highlights - this->parseHighlights(); - - // highlighting incoming whispers if requested per setting - if (this->args.isReceivedWhisper && getSettings()->highlightInlineWhispers) - { - this->message().flags.set(MessageFlag::HighlightedWhisper, true); - } - - return this->release(); -} - -void IrcMessageBuilder::appendUsername() -{ - QString username = this->userName; - this->message().loginName = username; - this->message().displayName = username; - - // The full string that will be rendered in the chat widget - QString usernameText = - SharedMessageBuilder::stylizeUsername(username, this->message()); - - if (this->args.isReceivedWhisper) - { - this->emplace(usernameText, MessageElementFlag::Username, - this->usernameColor_, - FontStyle::ChatMediumBold) - ->setLink({Link::UserWhisper, this->message().displayName}); - - // Separator - this->emplace("->", MessageElementFlag::Username, - MessageColor::System, FontStyle::ChatMedium); - - if (this->whisperTarget_.isEmpty()) - { - this->emplace("you:", MessageElementFlag::Username); - } - else - { - this->emplace(this->whisperTarget_ + ":", - MessageElementFlag::Username, - getRandomColor(this->whisperTarget_), - FontStyle::ChatMediumBold); - } - } - else if (this->args.isSentWhisper) - { - this->emplace(usernameText, MessageElementFlag::Username, - this->usernameColor_, - FontStyle::ChatMediumBold); - - // Separator - this->emplace("->", MessageElementFlag::Username, - MessageColor::System, FontStyle::ChatMedium); - - this->emplace( - this->whisperTarget_ + ":", MessageElementFlag::Username, - getRandomColor(this->whisperTarget_), FontStyle::ChatMediumBold) - ->setLink({Link::UserWhisper, this->whisperTarget_}); - } - else - { - if (!this->action_) - { - usernameText += ":"; - } - this->emplace(usernameText, MessageElementFlag::Username, - this->usernameColor_, - FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, this->message().loginName}); - } -} - -} // namespace chatterino diff --git a/src/providers/irc/IrcMessageBuilder.hpp b/src/providers/irc/IrcMessageBuilder.hpp deleted file mode 100644 index 820a893809f..00000000000 --- a/src/providers/irc/IrcMessageBuilder.hpp +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once - -#include "common/Aliases.hpp" -#include "common/Outcome.hpp" -#include "messages/SharedMessageBuilder.hpp" - -#include -#include -#include - -namespace chatterino { - -struct Emote; -using EmotePtr = std::shared_ptr; - -class Channel; - -class IrcMessageBuilder : public SharedMessageBuilder -{ -public: - IrcMessageBuilder() = delete; - - explicit IrcMessageBuilder(Channel *_channel, - const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args); - explicit IrcMessageBuilder(Channel *_channel, - const Communi::IrcMessage *_ircMessage, - const MessageParseArgs &_args, QString content, - bool isAction); - - /** - * @brief used for global notice messages (i.e. notice messages without a channel as its target) - **/ - explicit IrcMessageBuilder(const Communi::IrcNoticeMessage *_ircMessage, - const MessageParseArgs &_args); - - /** - * @brief used for whisper messages (i.e. PRIVMSG messages with our nick as the target) - **/ - explicit IrcMessageBuilder(const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args); - - MessagePtr build() override; - -private: - void appendUsername(); - - /** - * @brief holds the name of the target for the private/direct IRC message - * - * This might not be our nick - */ - QString whisperTarget_; -}; - -} // namespace chatterino diff --git a/src/providers/irc/IrcServer.cpp b/src/providers/irc/IrcServer.cpp deleted file mode 100644 index c88109ad439..00000000000 --- a/src/providers/irc/IrcServer.cpp +++ /dev/null @@ -1,386 +0,0 @@ -#include "providers/irc/IrcServer.hpp" - -#include "Application.hpp" -#include "common/QLogging.hpp" -#include "messages/Message.hpp" -#include "messages/MessageColor.hpp" -#include "messages/MessageElement.hpp" -#include "providers/irc/Irc2.hpp" -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcMessageBuilder.hpp" -#include "providers/twitch/TwitchIrcServer.hpp" // NOTE: Included to access the mentions channel -#include "singletons/Settings.hpp" -#include "util/IrcHelpers.hpp" - -#include -#include - -#include -#include - -namespace chatterino { - -IrcServer::IrcServer(const IrcServerData &data) - : data_(new IrcServerData(data)) -{ - this->initializeIrc(); - - this->connect(); -} - -IrcServer::IrcServer(const IrcServerData &data, - const std::vector> &restoreChannels) - : IrcServer(data) -{ - for (auto &&weak : restoreChannels) - { - if (auto shared = weak.lock()) - { - this->channels[shared->getName()] = weak; - } - } -} - -IrcServer::~IrcServer() -{ - delete this->data_; -} - -int IrcServer::id() -{ - return this->data_->id; -} - -const QString &IrcServer::user() -{ - return this->data_->user; -} - -const QString &IrcServer::nick() -{ - return this->data_->nick.isEmpty() ? this->data_->user : this->data_->nick; -} - -const QString &IrcServer::userFriendlyIdentifier() -{ - return this->data_->host; -} - -void IrcServer::initializeConnectionSignals(IrcConnection *connection, - ConnectionType type) -{ - assert(type == Both); - - QObject::connect( - connection, &Communi::IrcConnection::socketError, this, - [this](QAbstractSocket::SocketError error) { - static int index = - QAbstractSocket::staticMetaObject.indexOfEnumerator( - "SocketError"); - - std::lock_guard lock(this->channelMutex); - - for (auto &&weak : this->channels) - { - if (auto shared = weak.lock()) - { - shared->addSystemMessage( - QStringLiteral("Socket error: ") + - QAbstractSocket::staticMetaObject.enumerator(index) - .valueToKey(error)); - } - } - }); - - QObject::connect(connection, &Communi::IrcConnection::nickNameRequired, - this, [](const QString &reserved, QString *result) { - *result = QString("%1%2").arg( - reserved, QString::number(std::rand() % 100)); - }); - - QObject::connect(connection, &Communi::IrcConnection::noticeMessageReceived, - this, [this](Communi::IrcNoticeMessage *message) { - MessageParseArgs args; - args.isReceivedWhisper = true; - - IrcMessageBuilder builder(message, args); - - auto msg = builder.build(); - - for (auto &&weak : this->channels) - { - if (auto shared = weak.lock()) - { - shared->addMessage(msg, - MessageContext::Original); - } - } - }); - QObject::connect(connection, - &Communi::IrcConnection::capabilityMessageReceived, this, - [this](Communi::IrcCapabilityMessage *message) { - const QStringList caps = message->capabilities(); - if (caps.contains("echo-message")) - { - this->hasEcho_ = true; - } - }); -} - -void IrcServer::initializeConnection(IrcConnection *connection, - ConnectionType type) -{ - assert(type == Both); - - connection->setSecure(this->data_->ssl); - connection->setHost(this->data_->host); - connection->setPort(this->data_->port); - - connection->setUserName(this->data_->user); - connection->setNickName(this->data_->nick.isEmpty() ? this->data_->user - : this->data_->nick); - connection->setRealName(this->data_->real.isEmpty() ? this->data_->user - : this->data_->nick); - connection->network()->setRequestedCapabilities({"echo-message"}); - - if (getSettings()->enableExperimentalIrc) - { - switch (this->data_->authType) - { - case IrcAuthType::Sasl: - connection->setSaslMechanism("PLAIN"); - [[fallthrough]]; - case IrcAuthType::Pass: - this->data_->getPassword( - this, [conn = new QPointer(connection) /* can't copy */, - this](const QString &password) mutable { - if (*conn) - { - (*conn)->setPassword(password); - this->open(Both); - } - - delete conn; - }); - break; - default: - this->open(Both); - } - } -} - -std::shared_ptr IrcServer::createChannel(const QString &channelName) -{ - return std::make_shared(channelName, this); -} - -bool IrcServer::hasSeparateWriteConnection() const -{ - return false; -} - -void IrcServer::onReadConnected(IrcConnection *connection) -{ - { - std::lock_guard lock(this->channelMutex); - - for (auto &&command : this->data_->connectCommands) - { - connection->sendRaw(command + "\r\n"); - } - } - - AbstractIrcServer::onReadConnected(connection); -} - -void IrcServer::privateMessageReceived(Communi::IrcPrivateMessage *message) -{ - // Note: This doesn't use isPrivate() because it only applies to messages targeting our user, - // Servers or bouncers may send messages which have our user as the source - // (like with echo-message CAP), we need to take care of this. - if (!message->target().startsWith("#")) - { - MessageParseArgs args; - if (message->isOwn()) - { - // The server sent us a whisper which has our user as the source - args.isSentWhisper = true; - } - else - { - args.isReceivedWhisper = true; - } - - IrcMessageBuilder builder(message, args); - - auto msg = builder.build(); - - for (auto &&weak : this->channels) - { - if (auto shared = weak.lock()) - { - shared->addMessage(msg, MessageContext::Original); - } - } - return; - } - - auto target = message->target(); - target = target.startsWith('#') ? target.mid(1) : target; - - if (auto channel = this->getChannelOrEmpty(target); !channel->isEmpty()) - { - MessageParseArgs args; - IrcMessageBuilder builder(channel.get(), message, args); - - if (!builder.isIgnored()) - { - auto msg = builder.build(); - - channel->addMessage(msg, MessageContext::Original); - builder.triggerHighlights(); - const auto highlighted = msg->flags.has(MessageFlag::Highlighted); - const auto showInMentions = - msg->flags.has(MessageFlag::ShowInMentions); - - if (highlighted && showInMentions) - { - getApp()->getTwitch()->getMentionsChannel()->addMessage( - msg, MessageContext::Original); - } - } - else - { - qCDebug(chatterinoIrc) << "message ignored :rage:"; - } - } -} - -void IrcServer::readConnectionMessageReceived(Communi::IrcMessage *message) -{ - AbstractIrcServer::readConnectionMessageReceived(message); - - switch (message->type()) - { - case Communi::IrcMessage::Join: { - auto *x = static_cast(message); - - if (auto it = this->channels.find(x->channel()); - it != this->channels.end()) - { - if (auto shared = it->lock()) - { - if (message->nick() == this->data_->nick) - { - shared->addSystemMessage("joined"); - } - else - { - if (auto *c = - dynamic_cast(shared.get())) - { - c->addJoinedUser(x->nick()); - } - } - } - } - return; - } - - case Communi::IrcMessage::Part: { - auto *x = static_cast(message); - - if (auto it = this->channels.find(x->channel()); - it != this->channels.end()) - { - if (auto shared = it->lock()) - { - if (message->nick() == this->data_->nick) - { - shared->addSystemMessage("parted"); - } - else - { - if (auto *c = - dynamic_cast(shared.get())) - { - c->addPartedUser(x->nick()); - } - } - } - } - return; - } - - case Communi::IrcMessage::Pong: - case Communi::IrcMessage::Notice: - case Communi::IrcMessage::Private: - return; - - default: - if (getSettings()->showUnhandledIrcMessages) - { - MessageBuilder builder; - - builder.emplace( - calculateMessageTime(message).time()); - builder.emplace(message->toData(), - MessageElementFlag::Text); - builder->flags.set(MessageFlag::Debug); - - auto msg = builder.release(); - - for (auto &&weak : this->channels) - { - if (auto shared = weak.lock()) - { - shared->addMessage(msg, MessageContext::Original); - } - } - }; - } -} - -void IrcServer::sendWhisper(const QString &target, const QString &message) -{ - this->sendRawMessage(QString("PRIVMSG %1 :%2").arg(target, message)); - if (this->hasEcho()) - { - return; - } - - MessageParseArgs args; - args.isSentWhisper = true; - - MessageBuilder b; - - b.emplace(); - b.emplace(this->nick(), MessageElementFlag::Text, - MessageColor::Text, FontStyle::ChatMediumBold); - b.emplace("->", MessageElementFlag::Text, - MessageColor::System); - b.emplace(target + ":", MessageElementFlag::Text, - MessageColor::Text, FontStyle::ChatMediumBold); - b.emplace(message, MessageElementFlag::Text); - - auto msg = b.release(); - for (auto &&weak : this->channels) - { - if (auto shared = weak.lock()) - { - shared->addMessage(msg, MessageContext::Original); - } - } -} - -void IrcServer::sendRawMessage(const QString &rawMessage) -{ - AbstractIrcServer::sendRawMessage(rawMessage.left(510)); -} - -bool IrcServer::hasEcho() const -{ - return this->hasEcho_; -} - -} // namespace chatterino diff --git a/src/providers/irc/IrcServer.hpp b/src/providers/irc/IrcServer.hpp deleted file mode 100644 index 199a9340ab2..00000000000 --- a/src/providers/irc/IrcServer.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include "providers/irc/AbstractIrcServer.hpp" - -namespace chatterino { - -struct IrcServerData; - -class IrcServer : public AbstractIrcServer -{ -public: - explicit IrcServer(const IrcServerData &data); - IrcServer(const IrcServerData &data, - const std::vector> &restoreChannels); - ~IrcServer() override; - - int id(); - const QString &user(); - const QString &nick(); - const QString &userFriendlyIdentifier(); - - bool hasEcho() const; - /** - * @brief sends a whisper to the target user (PRIVMSG where a user is the target) - */ - void sendWhisper(const QString &target, const QString &message); - - void sendRawMessage(const QString &rawMessage) override; - - // AbstractIrcServer interface -protected: - void initializeConnectionSignals(IrcConnection *connection, - ConnectionType type) override; - void initializeConnection(IrcConnection *connection, - ConnectionType type) override; - std::shared_ptr createChannel(const QString &channelName) override; - bool hasSeparateWriteConnection() const override; - - void onReadConnected(IrcConnection *connection) override; - void privateMessageReceived(Communi::IrcPrivateMessage *message) override; - void readConnectionMessageReceived(Communi::IrcMessage *message) override; - -private: - // pointer so we don't have to circle include Irc2.hpp - IrcServerData *data_; - - bool hasEcho_{false}; -}; - -} // namespace chatterino diff --git a/src/providers/liveupdates/BasicPubSubClient.hpp b/src/providers/liveupdates/BasicPubSubClient.hpp index 31532c1ddd3..aa65e619a18 100644 --- a/src/providers/liveupdates/BasicPubSubClient.hpp +++ b/src/providers/liveupdates/BasicPubSubClient.hpp @@ -150,6 +150,15 @@ class BasicPubSubClient return this->started_.load(std::memory_order_acquire); } + /** + * @brief Will be called when the clients has been requested to stop + * + * Derived classes can override this to implement their own shutdown behaviour + */ + virtual void stopImpl() + { + } + liveupdates::WebsocketClient &websocketClient_; private: @@ -164,6 +173,8 @@ class BasicPubSubClient { assert(this->isStarted()); this->started_.store(false, std::memory_order_release); + + this->stopImpl(); } liveupdates::WebsocketHandle handle_; diff --git a/src/providers/liveupdates/BasicPubSubManager.hpp b/src/providers/liveupdates/BasicPubSubManager.hpp index f82f703631d..d7cb0253bd3 100644 --- a/src/providers/liveupdates/BasicPubSubManager.hpp +++ b/src/providers/liveupdates/BasicPubSubManager.hpp @@ -8,10 +8,12 @@ #include "providers/twitch/PubSubHelpers.hpp" #include "util/DebugCount.hpp" #include "util/ExponentialBackoff.hpp" +#include "util/RenameThread.hpp" #include #include #include +#include #include #include @@ -59,8 +61,9 @@ template class BasicPubSubManager { public: - BasicPubSubManager(QString host) + BasicPubSubManager(QString host, QString shortName) : host_(std::move(host)) + , shortName_(std::move(shortName)) { this->websocketClient_.set_access_channels( websocketpp::log::alevel::all); @@ -94,7 +97,10 @@ class BasicPubSubManager .toStdString()); } - virtual ~BasicPubSubManager() = default; + virtual ~BasicPubSubManager() + { + this->stop(); + } BasicPubSubManager(const BasicPubSubManager &) = delete; BasicPubSubManager(const BasicPubSubManager &&) = delete; @@ -115,6 +121,8 @@ class BasicPubSubManager this->mainThread_.reset(new std::thread([this] { runThread(); })); + + renameThread(*this->mainThread_.get(), "BPSM-" % this->shortName_); } void stop() @@ -373,6 +381,9 @@ class BasicPubSubManager const QString host_; + /// Short name of the service (e.g. "7TV" or "BTTV") + const QString shortName_; + bool stopping_{false}; }; diff --git a/src/providers/seventv/SeventvEventAPI.cpp b/src/providers/seventv/SeventvEventAPI.cpp index 94430d31b08..b298f310c40 100644 --- a/src/providers/seventv/SeventvEventAPI.cpp +++ b/src/providers/seventv/SeventvEventAPI.cpp @@ -1,6 +1,7 @@ #include "providers/seventv/SeventvEventAPI.hpp" #include "Application.hpp" +#include "common/Literals.hpp" #include "providers/seventv/eventapi/Client.hpp" #include "providers/seventv/eventapi/Dispatch.hpp" #include "providers/seventv/eventapi/Message.hpp" @@ -17,10 +18,11 @@ namespace chatterino { using namespace seventv; using namespace seventv::eventapi; +using namespace chatterino::literals; SeventvEventAPI::SeventvEventAPI( QString host, std::chrono::milliseconds defaultHeartbeatInterval) - : BasicPubSubManager(std::move(host)) + : BasicPubSubManager(std::move(host), u"7TV"_s) , heartbeatInterval_(defaultHeartbeatInterval) { } diff --git a/src/providers/seventv/SeventvEventAPI.hpp b/src/providers/seventv/SeventvEventAPI.hpp index 084ad0de887..425b21bc137 100644 --- a/src/providers/seventv/SeventvEventAPI.hpp +++ b/src/providers/seventv/SeventvEventAPI.hpp @@ -2,6 +2,7 @@ #include "providers/liveupdates/BasicPubSubClient.hpp" #include "providers/liveupdates/BasicPubSubManager.hpp" +#include "providers/seventv/eventapi/Subscription.hpp" #include "util/QStringHash.hpp" #include @@ -9,7 +10,6 @@ namespace chatterino { namespace seventv::eventapi { - struct Subscription; struct Dispatch; struct EmoteAddDispatch; struct EmoteUpdateDispatch; diff --git a/src/providers/seventv/eventapi/Client.cpp b/src/providers/seventv/eventapi/Client.cpp index f266478ce3d..84533a066bc 100644 --- a/src/providers/seventv/eventapi/Client.cpp +++ b/src/providers/seventv/eventapi/Client.cpp @@ -13,9 +13,16 @@ Client::Client(liveupdates::WebsocketClient &websocketClient, : BasicPubSubClient(websocketClient, std::move(handle)) , lastHeartbeat_(std::chrono::steady_clock::now()) , heartbeatInterval_(heartbeatInterval) + , heartbeatTimer_(std::make_shared( + this->websocketClient_.get_io_service())) { } +void Client::stopImpl() +{ + this->heartbeatTimer_->cancel(); +} + void Client::onConnectionEstablished() { this->lastHeartbeat_.store(std::chrono::steady_clock::now(), @@ -54,14 +61,13 @@ void Client::checkHeartbeat() auto self = std::dynamic_pointer_cast(this->shared_from_this()); - runAfter(this->websocketClient_.get_io_service(), this->heartbeatInterval_, - [self](auto) { - if (!self->isStarted()) - { - return; - } - self->checkHeartbeat(); - }); + runAfter(this->heartbeatTimer_, this->heartbeatInterval_, [self](auto) { + if (!self->isStarted()) + { + return; + } + self->checkHeartbeat(); + }); } } // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Client.hpp b/src/providers/seventv/eventapi/Client.hpp index 11683edcf05..ecbc7cb7398 100644 --- a/src/providers/seventv/eventapi/Client.hpp +++ b/src/providers/seventv/eventapi/Client.hpp @@ -19,6 +19,8 @@ class Client : public BasicPubSubClient liveupdates::WebsocketHandle handle, std::chrono::milliseconds heartbeatInterval); + void stopImpl() override; + void setHeartbeatInterval(int intervalMs); void handleHeartbeat(); @@ -32,6 +34,7 @@ class Client : public BasicPubSubClient lastHeartbeat_; // This will be set once on the welcome message. std::chrono::milliseconds heartbeatInterval_; + std::shared_ptr heartbeatTimer_; friend SeventvEventAPI; }; diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 7660cd1158b..d0b284bb24d 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -14,7 +14,6 @@ #include "messages/MessageColor.hpp" #include "messages/MessageElement.hpp" #include "messages/MessageThread.hpp" -#include "providers/irc/AbstractIrcServer.hpp" #include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchAccountManager.hpp" @@ -171,7 +170,7 @@ void updateReplyParticipatedStatus(const QVariantMap &tags, } ChannelPtr channelOrEmptyByTarget(const QString &target, - IAbstractIrcServer &server) + ITwitchIrcServer &server) { QString channelName; if (!trimChannelName(target, channelName)) @@ -679,10 +678,9 @@ std::vector IrcMessageHandler::parseMessageWithReply( } void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, - ITwitchIrcServer &twitchServer, - IAbstractIrcServer &abstractIrcServer) + ITwitchIrcServer &twitchServer) { - auto chan = channelOrEmptyByTarget(message->target(), abstractIrcServer); + auto chan = channelOrEmptyByTarget(message->target(), twitchServer); if (chan->isEmpty()) { return; @@ -988,9 +986,8 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *ircMessage) } } -void IrcMessageHandler::handleUserNoticeMessage( - Communi::IrcMessage *message, ITwitchIrcServer &twitchServer, - IAbstractIrcServer &abstractIrcServer) +void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, + ITwitchIrcServer &twitchServer) { auto tags = message->tags(); auto parameters = message->parameters(); @@ -1003,7 +1000,7 @@ void IrcMessageHandler::handleUserNoticeMessage( content = parameters[1]; } - auto chn = abstractIrcServer.getChannelOrEmpty(target); + auto chn = twitchServer.getChannelOrEmpty(target); if (isIgnoredMessage({ .message = content, .twitchUserID = tags.value("user-id").toString(), @@ -1096,7 +1093,7 @@ void IrcMessageHandler::handleUserNoticeMessage( return; } - auto chan = abstractIrcServer.getChannelOrEmpty(channelName); + auto chan = twitchServer.getChannelOrEmpty(channelName); if (!chan->isEmpty()) { diff --git a/src/providers/twitch/IrcMessageHandler.hpp b/src/providers/twitch/IrcMessageHandler.hpp index 60dbacaf356..705f39ba383 100644 --- a/src/providers/twitch/IrcMessageHandler.hpp +++ b/src/providers/twitch/IrcMessageHandler.hpp @@ -9,7 +9,6 @@ namespace chatterino { -class IAbstractIrcServer; class ITwitchIrcServer; class Channel; using ChannelPtr = std::shared_ptr; @@ -39,8 +38,7 @@ class IrcMessageHandler std::vector &otherLoaded); void handlePrivMessage(Communi::IrcPrivateMessage *message, - ITwitchIrcServer &twitchServer, - IAbstractIrcServer &abstractIrcServer); + ITwitchIrcServer &twitchServer); void handleRoomStateMessage(Communi::IrcMessage *message); void handleClearChatMessage(Communi::IrcMessage *message); @@ -50,8 +48,7 @@ class IrcMessageHandler void handleWhisperMessage(Communi::IrcMessage *ircMessage); void handleUserNoticeMessage(Communi::IrcMessage *message, - ITwitchIrcServer &twitchServer, - IAbstractIrcServer &abstractIrcServer); + ITwitchIrcServer &twitchServer); void handleNoticeMessage(Communi::IrcNoticeMessage *message); diff --git a/src/providers/twitch/PubSubManager.cpp b/src/providers/twitch/PubSubManager.cpp index 9f16951a7fd..c8050a24dbe 100644 --- a/src/providers/twitch/PubSubManager.cpp +++ b/src/providers/twitch/PubSubManager.cpp @@ -10,6 +10,7 @@ #include "util/DebugCount.hpp" #include "util/Helpers.hpp" #include "util/RapidjsonHelpers.hpp" +#include "util/RenameThread.hpp" #include @@ -17,6 +18,7 @@ #include #include #include +#include #include using websocketpp::lib::bind; @@ -543,7 +545,10 @@ void PubSub::start() { this->work = std::make_shared( this->websocketClient.get_io_service()); - this->thread.reset(new std::thread(std::bind(&PubSub::runThread, this))); + this->thread = std::make_unique([this] { + runThread(); + }); + renameThread(*this->thread, "PubSub"); } void PubSub::stop() diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 580558ae5ed..587b220237d 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -9,7 +9,6 @@ #include "debug/AssertInGuiThread.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" -#include "providers/irc/IrcMessageBuilder.hpp" #include "providers/IvrApi.hpp" #include "providers/seventv/SeventvAPI.hpp" #include "providers/seventv/SeventvEmotes.hpp" diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 2aa587fa41c..9a1c6277d51 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -149,15 +149,14 @@ TwitchChannel::~TwitchChannel() getApp()->getTwitch()->dropSeventvChannel(this->seventvUserID_, this->seventvEmoteSetID_); - if (getApp()->getTwitch()->getBTTVLiveUpdates()) + if (getApp()->getBttvLiveUpdates()) { - getApp()->getTwitch()->getBTTVLiveUpdates()->partChannel( - this->roomId()); + getApp()->getBttvLiveUpdates()->partChannel(this->roomId()); } - if (getApp()->getTwitch()->getSeventvEventAPI()) + if (getApp()->getSeventvEventAPI()) { - getApp()->getTwitch()->getSeventvEventAPI()->unsubscribeTwitchChannel( + getApp()->getSeventvEventAPI()->unsubscribeTwitchChannel( this->roomId()); } } @@ -857,7 +856,7 @@ const QString &TwitchChannel::seventvEmoteSetID() const void TwitchChannel::joinBttvChannel() const { - if (getApp()->getTwitch()->getBTTVLiveUpdates()) + if (getApp()->getBttvLiveUpdates()) { const auto currentAccount = getApp()->getAccounts()->twitch.getCurrent(); @@ -866,8 +865,7 @@ void TwitchChannel::joinBttvChannel() const { userName = currentAccount->getUserName(); } - getApp()->getTwitch()->getBTTVLiveUpdates()->joinChannel(this->roomId(), - userName); + getApp()->getBttvLiveUpdates()->joinChannel(this->roomId(), userName); } } @@ -1015,9 +1013,9 @@ void TwitchChannel::updateSeventvData(const QString &newUserID, this->seventvUserID_ = newUserID; this->seventvEmoteSetID_ = newEmoteSetID; runInGuiThread([this, oldUserID, oldEmoteSetID]() { - if (getApp()->getTwitch()->getSeventvEventAPI()) + if (getApp()->getSeventvEventAPI()) { - getApp()->getTwitch()->getSeventvEventAPI()->subscribeUser( + getApp()->getSeventvEventAPI()->subscribeUser( this->seventvUserID_, this->seventvEmoteSetID_); if (oldUserID || oldEmoteSetID) @@ -1814,10 +1812,9 @@ void TwitchChannel::updateSevenTVActivity() void TwitchChannel::listenSevenTVCosmetics() const { - if (getApp()->getTwitch()->getSeventvEventAPI()) + if (getApp()->getSeventvEventAPI()) { - getApp()->getTwitch()->getSeventvEventAPI()->subscribeTwitchChannel( - this->roomId()); + getApp()->getSeventvEventAPI()->subscribeTwitchChannel(this->roomId()); } } diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index dc12d562b94..583bd810703 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -2,39 +2,46 @@ #include "Application.hpp" #include "common/Channel.hpp" +#include "common/Common.hpp" #include "common/Env.hpp" #include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" +#include "messages/LimitedQueueSnapshot.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "providers/bttv/BttvEmotes.hpp" -#include "providers/bttv/BttvLiveUpdates.hpp" #include "providers/ffz/FfzEmotes.hpp" -#include "providers/seventv/eventapi/Subscription.hpp" +#include "providers/irc/IrcConnection2.hpp" #include "providers/seventv/SeventvEmotes.hpp" #include "providers/seventv/SeventvEventAPI.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/TwitchChannel.hpp" #include "singletons/Settings.hpp" -#include "util/Helpers.hpp" #include "util/PostToThread.hpp" +#include "util/RatelimitBucket.hpp" #include +#include +#include +#include +#include #include #include +#include +#include using namespace std::chrono_literals; namespace { -using namespace chatterino; +// Ratelimits for joinBucket_ +constexpr int JOIN_RATELIMIT_BUDGET = 18; +constexpr int JOIN_RATELIMIT_COOLDOWN = 12500; -const QString BTTV_LIVE_UPDATES_URL = "wss://sockets.betterttv.net/ws"; -const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3"; +using namespace chatterino; void sendHelixMessage(const std::shared_ptr &channel, const QString &message, const QString &replyParentId = {}) @@ -143,26 +150,77 @@ TwitchIrcServer::TwitchIrcServer() , automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod)) , watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching) { - this->initializeIrc(); - - if (getSettings()->enableBTTVLiveUpdates && - getSettings()->enableBTTVChannelEmotes) - { - this->bttvLiveUpdates = - std::make_unique(BTTV_LIVE_UPDATES_URL); - } - - if (getSettings()->enableSevenTVEventAPI && - getSettings()->enableSevenTVChannelEmotes) - { - this->seventvEventAPI = - std::make_unique(SEVENTV_EVENTAPI_URL); - } + // Initialize the connections + // XXX: don't create write connection if there is no separate write connection. + this->writeConnection_.reset(new IrcConnection); + this->writeConnection_->moveToThread( + QCoreApplication::instance()->thread()); + + // Apply a leaky bucket rate limiting to JOIN messages + auto actuallyJoin = [&](QString message) { + if (!this->channels.contains(message)) + { + return; + } + this->readConnection_->sendRaw("JOIN #" + message); + }; + this->joinBucket_.reset(new RatelimitBucket( + JOIN_RATELIMIT_BUDGET, JOIN_RATELIMIT_COOLDOWN, actuallyJoin, this)); + + QObject::connect(this->writeConnection_.get(), + &Communi::IrcConnection::messageReceived, this, + [this](auto msg) { + this->writeConnectionMessageReceived(msg); + }); + QObject::connect(this->writeConnection_.get(), + &Communi::IrcConnection::connected, this, [this] { + this->onWriteConnected(this->writeConnection_.get()); + }); + this->connections_.managedConnect( + this->writeConnection_->connectionLost, [this](bool timeout) { + qCDebug(chatterinoIrc) + << "Write connection reconnect requested. Timeout:" << timeout; + this->writeConnection_->smartReconnect(); + }); - // getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) { - // this->connect(); }, - // this->signalHolder_, - // false); + // Listen to read connection message signals + this->readConnection_.reset(new IrcConnection); + this->readConnection_->moveToThread(QCoreApplication::instance()->thread()); + + QObject::connect(this->readConnection_.get(), + &Communi::IrcConnection::messageReceived, this, + [this](auto msg) { + this->readConnectionMessageReceived(msg); + }); + QObject::connect(this->readConnection_.get(), + &Communi::IrcConnection::privateMessageReceived, this, + [this](auto msg) { + this->privateMessageReceived(msg); + }); + QObject::connect(this->readConnection_.get(), + &Communi::IrcConnection::connected, this, [this] { + this->onReadConnected(this->readConnection_.get()); + }); + QObject::connect(this->readConnection_.get(), + &Communi::IrcConnection::disconnected, this, [this] { + this->onDisconnected(); + }); + this->connections_.managedConnect( + this->readConnection_->connectionLost, [this](bool timeout) { + qCDebug(chatterinoIrc) + << "Read connection reconnect requested. Timeout:" << timeout; + if (timeout) + { + // Show additional message since this is going to interrupt a + // connection that is still "connected" + this->addGlobalSystemMessage( + "Server connection timed out, reconnecting"); + } + this->readConnection_->smartReconnect(); + }); + this->connections_.managedConnect(this->readConnection_->heartbeat, [this] { + this->markChannelsConnected(); + }); } void TwitchIrcServer::initialize() @@ -263,14 +321,12 @@ std::shared_ptr TwitchIrcServer::createChannel( void TwitchIrcServer::privateMessageReceived( Communi::IrcPrivateMessage *message) { - IrcMessageHandler::instance().handlePrivMessage(message, *this, *this); + IrcMessageHandler::instance().handlePrivMessage(message, *this); } void TwitchIrcServer::readConnectionMessageReceived( Communi::IrcMessage *message) { - AbstractIrcServer::readConnectionMessageReceived(message); - if (message->type() == Communi::IrcMessage::Type::Private) { // We already have a handler for private messages @@ -310,7 +366,7 @@ void TwitchIrcServer::readConnectionMessageReceived( } else if (command == "USERNOTICE") { - handler.handleUserNoticeMessage(message, *this, *this); + handler.handleUserNoticeMessage(message, *this); } else if (command == "NOTICE") { @@ -361,6 +417,84 @@ void TwitchIrcServer::writeConnectionMessageReceived( } } +void TwitchIrcServer::onReadConnected(IrcConnection *connection) +{ + (void)connection; + + std::lock_guard lock(this->channelMutex); + + // join channels + for (auto &&weak : this->channels) + { + if (auto channel = weak.lock()) + { + this->joinBucket_->send(channel->getName()); + } + } + + // connected/disconnected message + auto connectedMsg = makeSystemMessage("connected"); + connectedMsg->flags.set(MessageFlag::ConnectedMessage); + auto reconnected = makeSystemMessage("reconnected"); + reconnected->flags.set(MessageFlag::ConnectedMessage); + + for (std::weak_ptr &weak : this->channels.values()) + { + std::shared_ptr chan = weak.lock(); + if (!chan) + { + continue; + } + + LimitedQueueSnapshot snapshot = chan->getMessageSnapshot(); + + bool replaceMessage = + snapshot.size() > 0 && snapshot[snapshot.size() - 1]->flags.has( + MessageFlag::DisconnectedMessage); + + if (replaceMessage) + { + chan->replaceMessage(snapshot[snapshot.size() - 1], reconnected); + } + else + { + chan->addMessage(connectedMsg, MessageContext::Original); + } + } + + this->falloffCounter_ = 1; +} + +void TwitchIrcServer::onWriteConnected(IrcConnection *connection) +{ + (void)connection; +} + +void TwitchIrcServer::onDisconnected() +{ + std::lock_guard lock(this->channelMutex); + + MessageBuilder b(systemMessage, "disconnected"); + b->flags.set(MessageFlag::DisconnectedMessage); + auto disconnectedMsg = b.release(); + + for (std::weak_ptr &weak : this->channels.values()) + { + std::shared_ptr chan = weak.lock(); + if (!chan) + { + continue; + } + + chan->addMessage(disconnectedMsg, MessageContext::Original); + + if (auto *channel = dynamic_cast(chan.get())) + { + channel->markDisconnected(); + } + } +} + std::shared_ptr TwitchIrcServer::getCustomChannel( const QString &channelName) { @@ -538,12 +672,6 @@ QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName) } } -bool TwitchIrcServer::hasSeparateWriteConnection() const -{ - return true; - // return getSettings()->twitchSeperateWriteConnection; -} - bool TwitchIrcServer::prepareToSend( const std::shared_ptr &channel) { @@ -638,16 +766,6 @@ void TwitchIrcServer::onReplySendRequested( sent = true; } -std::unique_ptr &TwitchIrcServer::getBTTVLiveUpdates() -{ - return this->bttvLiveUpdates; -} - -std::unique_ptr &TwitchIrcServer::getSeventvEventAPI() -{ - return this->seventvEventAPI; -} - const IndirectChannel &TwitchIrcServer::getWatchingChannel() const { return this->watchingChannel; @@ -759,7 +877,7 @@ void TwitchIrcServer::forEachSeventvUser( void TwitchIrcServer::dropSeventvChannel(const QString &userID, const QString &emoteSetID) { - if (!this->seventvEventAPI) + if (!getApp()->getSeventvEventAPI()) { return; } @@ -798,11 +916,195 @@ void TwitchIrcServer::dropSeventvChannel(const QString &userID, if (!foundUser) { - this->seventvEventAPI->unsubscribeUser(userID); + getApp()->getSeventvEventAPI()->unsubscribeUser(userID); } if (!foundSet) { - this->seventvEventAPI->unsubscribeEmoteSet(emoteSetID); + getApp()->getSeventvEventAPI()->unsubscribeEmoteSet(emoteSetID); + } +} + +void TwitchIrcServer::markChannelsConnected() +{ + this->forEachChannel([](const ChannelPtr &chan) { + if (auto *channel = dynamic_cast(chan.get())) + { + channel->markConnected(); + } + }); +} + +void TwitchIrcServer::addFakeMessage(const QString &data) +{ + auto *fakeMessage = Communi::IrcMessage::fromData( + data.toUtf8(), this->readConnection_.get()); + + if (fakeMessage->command() == "PRIVMSG") + { + this->privateMessageReceived( + static_cast(fakeMessage)); + } + else + { + this->readConnectionMessageReceived(fakeMessage); + } +} + +void TwitchIrcServer::addGlobalSystemMessage(const QString &messageText) +{ + std::lock_guard lock(this->channelMutex); + + MessageBuilder b(systemMessage, messageText); + auto message = b.release(); + + for (std::weak_ptr &weak : this->channels.values()) + { + std::shared_ptr chan = weak.lock(); + if (!chan) + { + continue; + } + + chan->addMessage(message, MessageContext::Original); + } +} + +void TwitchIrcServer::forEachChannel(std::function func) +{ + std::lock_guard lock(this->channelMutex); + + for (std::weak_ptr &weak : this->channels.values()) + { + ChannelPtr chan = weak.lock(); + if (!chan) + { + continue; + } + + func(chan); + } +} + +void TwitchIrcServer::connect() +{ + this->disconnect(); + + this->initializeConnection(this->writeConnection_.get(), + ConnectionType::Write); + this->initializeConnection(this->readConnection_.get(), + ConnectionType::Read); +} + +void TwitchIrcServer::disconnect() +{ + std::lock_guard locker(this->connectionMutex_); + + this->readConnection_->close(); + this->writeConnection_->close(); +} + +void TwitchIrcServer::sendMessage(const QString &channelName, + const QString &message) +{ + this->sendRawMessage("PRIVMSG #" + channelName + " :" + message); +} + +void TwitchIrcServer::sendRawMessage(const QString &rawMessage) +{ + std::lock_guard locker(this->connectionMutex_); + + this->writeConnection_->sendRaw(rawMessage); +} + +ChannelPtr TwitchIrcServer::getOrAddChannel(const QString &dirtyChannelName) +{ + auto channelName = this->cleanChannelName(dirtyChannelName); + + // try get channel + ChannelPtr chan = this->getChannelOrEmpty(channelName); + if (chan != Channel::getEmpty()) + { + return chan; + } + + std::lock_guard lock(this->channelMutex); + + // value doesn't exist + chan = this->createChannel(channelName); + if (!chan) + { + return Channel::getEmpty(); + } + + this->channels.insert(channelName, chan); + this->connections_.managedConnect(chan->destroyed, [this, channelName] { + // fourtf: issues when the server itself is destroyed + + qCDebug(chatterinoIrc) << "[TwitchIrcServer::addChannel]" << channelName + << "was destroyed"; + this->channels.remove(channelName); + + if (this->readConnection_) + { + this->readConnection_->sendRaw("PART #" + channelName); + } + }); + + // join IRC channel + { + std::lock_guard lock2(this->connectionMutex_); + + if (this->readConnection_) + { + if (this->readConnection_->isConnected()) + { + this->joinBucket_->send(channelName); + } + } + } + + return chan; +} + +ChannelPtr TwitchIrcServer::getChannelOrEmpty(const QString &dirtyChannelName) +{ + auto channelName = this->cleanChannelName(dirtyChannelName); + + std::lock_guard lock(this->channelMutex); + + // try get special channel + ChannelPtr chan = this->getCustomChannel(channelName); + if (chan) + { + return chan; + } + + // value exists + auto it = this->channels.find(channelName); + if (it != this->channels.end()) + { + chan = it.value().lock(); + + if (chan) + { + return chan; + } + } + + return Channel::getEmpty(); +} + +void TwitchIrcServer::open(ConnectionType type) +{ + std::lock_guard lock(this->connectionMutex_); + + if (type == ConnectionType::Write) + { + this->writeConnection_->open(); + } + if (type == ConnectionType::Read) + { + this->readConnection_->open(); } } diff --git a/src/providers/twitch/TwitchIrcServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp index 1f3dbe73028..e47f9f4ee68 100644 --- a/src/providers/twitch/TwitchIrcServer.hpp +++ b/src/providers/twitch/TwitchIrcServer.hpp @@ -2,12 +2,18 @@ #include "common/Atomic.hpp" #include "common/Channel.hpp" -#include "providers/irc/AbstractIrcServer.hpp" +#include "common/Common.hpp" +#include "providers/irc/IrcConnection2.hpp" +#include "util/RatelimitBucket.hpp" +#include +#include #include #include +#include #include +#include #include namespace chatterino { @@ -15,16 +21,33 @@ namespace chatterino { class Settings; class Paths; class TwitchChannel; -class BttvLiveUpdates; -class SeventvEventAPI; class BttvEmotes; class FfzEmotes; class SeventvEmotes; +class RatelimitBucket; class ITwitchIrcServer { public: + ITwitchIrcServer() = default; virtual ~ITwitchIrcServer() = default; + ITwitchIrcServer(const ITwitchIrcServer &) = delete; + ITwitchIrcServer(ITwitchIrcServer &&) = delete; + ITwitchIrcServer &operator=(const ITwitchIrcServer &) = delete; + ITwitchIrcServer &operator=(ITwitchIrcServer &&) = delete; + + virtual void connect() = 0; + + virtual void sendRawMessage(const QString &rawMessage) = 0; + + virtual ChannelPtr getOrAddChannel(const QString &dirtyChannelName) = 0; + virtual ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) = 0; + + virtual void addFakeMessage(const QString &data) = 0; + + virtual void addGlobalSystemMessage(const QString &messageText) = 0; + + virtual void forEachChannel(std::function func) = 0; virtual void forEachChannelAndSpecialChannels( std::function func) = 0; @@ -35,9 +58,6 @@ class ITwitchIrcServer virtual void dropSeventvChannel(const QString &userID, const QString &emoteSetID) = 0; - virtual std::unique_ptr &getBTTVLiveUpdates() = 0; - virtual std::unique_ptr &getSeventvEventAPI() = 0; - virtual const IndirectChannel &getWatchingChannel() const = 0; virtual void setWatchingChannel(ChannelPtr newWatchingChannel) = 0; virtual ChannelPtr getWhispersChannel() const = 0; @@ -51,9 +71,14 @@ class ITwitchIrcServer // Update this interface with TwitchIrcServer methods as needed }; -class TwitchIrcServer final : public AbstractIrcServer, public ITwitchIrcServer +class TwitchIrcServer final : public ITwitchIrcServer, public QObject { public: + enum class ConnectionType { + Read, + Write, + }; + TwitchIrcServer(); ~TwitchIrcServer() override = default; @@ -93,6 +118,25 @@ class TwitchIrcServer final : public AbstractIrcServer, public ITwitchIrcServer void dropSeventvChannel(const QString &userID, const QString &emoteSetID) override; + void addFakeMessage(const QString &data) override; + + void addGlobalSystemMessage(const QString &messageText) override; + + // iteration + void forEachChannel(std::function func) override; + + void connect() override; + void disconnect(); + + void sendMessage(const QString &channelName, const QString &message); + void sendRawMessage(const QString &rawMessage) override; + + ChannelPtr getOrAddChannel(const QString &dirtyChannelName) override; + + ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) override; + + void open(ConnectionType type); + private: Atomic lastUserThatWhisperedMe; @@ -102,13 +146,7 @@ class TwitchIrcServer final : public AbstractIrcServer, public ITwitchIrcServer const ChannelPtr automodChannel; IndirectChannel watchingChannel; - std::unique_ptr bttvLiveUpdates; - std::unique_ptr seventvEventAPI; - public: - std::unique_ptr &getBTTVLiveUpdates() override; - std::unique_ptr &getSeventvEventAPI() override; - const IndirectChannel &getWatchingChannel() const override; void setWatchingChannel(ChannelPtr newWatchingChannel) override; ChannelPtr getWhispersChannel() const override; @@ -120,19 +158,21 @@ class TwitchIrcServer final : public AbstractIrcServer, public ITwitchIrcServer void setLastUserThatWhisperedMe(const QString &user) override; protected: - void initializeConnection(IrcConnection *connection, - ConnectionType type) override; - std::shared_ptr createChannel(const QString &channelName) override; + void initializeConnection(IrcConnection *connection, ConnectionType type); + std::shared_ptr createChannel(const QString &channelName); + + void privateMessageReceived(Communi::IrcPrivateMessage *message); + void readConnectionMessageReceived(Communi::IrcMessage *message); + void writeConnectionMessageReceived(Communi::IrcMessage *message); - void privateMessageReceived(Communi::IrcPrivateMessage *message) override; - void readConnectionMessageReceived(Communi::IrcMessage *message) override; - void writeConnectionMessageReceived(Communi::IrcMessage *message) override; + void onReadConnected(IrcConnection *connection); + void onWriteConnected(IrcConnection *connection); + void onDisconnected(); + void markChannelsConnected(); - std::shared_ptr getCustomChannel( - const QString &channelname) override; + std::shared_ptr getCustomChannel(const QString &channelname); - QString cleanChannelName(const QString &dirtyChannelName) override; - bool hasSeparateWriteConnection() const override; + QString cleanChannelName(const QString &dirtyChannelName); private: void onMessageSendRequested(const std::shared_ptr &channel, @@ -143,6 +183,23 @@ class TwitchIrcServer final : public AbstractIrcServer, public ITwitchIrcServer bool prepareToSend(const std::shared_ptr &channel); + QMap> channels; + std::mutex channelMutex; + + QObjectPtr writeConnection_ = nullptr; + QObjectPtr readConnection_ = nullptr; + + // Our rate limiting bucket for the Twitch join rate limits + // https://dev.twitch.tv/docs/irc/guide#rate-limits + QObjectPtr joinBucket_; + + QTimer reconnectTimer_; + int falloffCounter_ = 1; + + std::mutex connectionMutex_; + + pajlada::Signals::SignalHolder connections_; + std::mutex lastMessageMutex_; std::queue lastMessagePleb_; std::queue lastMessageMod_; diff --git a/src/singletons/NativeMessaging.cpp b/src/singletons/NativeMessaging.cpp index 58dfd1b2b37..72dc1c3b602 100644 --- a/src/singletons/NativeMessaging.cpp +++ b/src/singletons/NativeMessaging.cpp @@ -134,6 +134,22 @@ namespace nm::client { NativeMessagingServer::NativeMessagingServer() : thread(*this) { + this->thread.setObjectName("NativeMessagingReceiver"); +} + +NativeMessagingServer::~NativeMessagingServer() +{ + if (!ipc::IpcQueue::remove("chatterino_gui")) + { + qCWarning(chatterinoNativeMessage) << "Failed to remove message queue"; + } + this->thread.requestInterruption(); + this->thread.quit(); + // Most likely, the receiver thread will still wait for a message + if (!this->thread.wait(250)) + { + this->thread.terminate(); + } } void NativeMessagingServer::start() @@ -161,7 +177,7 @@ void NativeMessagingServer::ReceiverThread::run() return; } - while (true) + while (!this->isInterruptionRequested()) { auto buf = messageQueue->receive(); if (buf.isEmpty()) diff --git a/src/singletons/NativeMessaging.hpp b/src/singletons/NativeMessaging.hpp index 5d0633358db..0d3ba454f0d 100644 --- a/src/singletons/NativeMessaging.hpp +++ b/src/singletons/NativeMessaging.hpp @@ -36,6 +36,7 @@ class NativeMessagingServer final NativeMessagingServer(NativeMessagingServer &&) = delete; NativeMessagingServer &operator=(const NativeMessagingServer &) = delete; NativeMessagingServer &operator=(NativeMessagingServer &&) = delete; + ~NativeMessagingServer(); void start(); diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 681cd28ec78..04e13a12dfa 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -522,7 +522,6 @@ class Settings #ifdef Q_OS_LINUX BoolSetting useKeyring = {"/misc/useKeyring", true}; #endif - BoolSetting enableExperimentalIrc = {"/misc/experimentalIrc", false}; IntSetting startUpNotification = {"/misc/startUpNotification", 0}; QStringSetting currentVersion = {"/misc/currentVersion", ""}; @@ -561,14 +560,7 @@ class Settings true}; BoolSetting lockNotebookLayout = {"/misc/lockNotebookLayout", false}; - /// Debug - BoolSetting showUnhandledIrcMessages = {"/debug/showUnhandledIrcMessages", - false}; - /// UI - // Purely QOL settings are here (like last item in a list). - IntSetting lastSelectChannelTab = {"/ui/lastSelectChannelTab", 0}; - IntSetting lastSelectIrcConn = {"/ui/lastSelectIrcConn", 0}; BoolSetting showSendButton = {"/ui/showSendButton", false}; diff --git a/src/singletons/StreamerMode.cpp b/src/singletons/StreamerMode.cpp index 659bfd9ea0f..fac58aaade1 100644 --- a/src/singletons/StreamerMode.cpp +++ b/src/singletons/StreamerMode.cpp @@ -105,6 +105,12 @@ bool isBroadcasterSoftwareActive() break; } + if (!p.waitForFinished(1000)) + { + qCWarning(chatterinoStreamerMode) << "Force-killing pgrep"; + p.kill(); + } + return false; #elif defined(Q_OS_WIN) if (!IsWindowsVistaOrGreater()) @@ -152,9 +158,16 @@ class StreamerModePrivate { public: StreamerModePrivate(StreamerMode *parent_); + ~StreamerModePrivate(); + StreamerModePrivate(const StreamerModePrivate &) = delete; + StreamerModePrivate(StreamerModePrivate &&) = delete; + StreamerModePrivate &operator=(const StreamerModePrivate &) = delete; + StreamerModePrivate &operator=(StreamerModePrivate &&) = delete; [[nodiscard]] bool isEnabled() const; + void start(); + private: void settingChanged(StreamerModeSetting value); void setEnabled(bool enabled); @@ -189,9 +202,15 @@ bool StreamerMode::isEnabled() const return this->private_->isEnabled(); } +void StreamerMode::start() +{ + this->private_->start(); +} + StreamerModePrivate::StreamerModePrivate(StreamerMode *parent) : parent_(parent) { + this->thread_.setObjectName("StreamerMode"); this->timer_.moveToThread(&this->thread_); QObject::connect(&this->timer_, &QTimer::timeout, [this] { auto timeouts = @@ -216,9 +235,24 @@ StreamerModePrivate::StreamerModePrivate(StreamerMode *parent) QObject::connect(&this->thread_, &QThread::started, [this] { this->settingChanged(getSettings()->enableStreamerMode.getEnum()); }); +} + +void StreamerModePrivate::start() +{ this->thread_.start(); } +StreamerModePrivate::~StreamerModePrivate() +{ + this->thread_.quit(); + if (!this->thread_.wait(500)) + { + qCWarning(chatterinoStreamerMode) + << "Failed waiting for thread, terminating it"; + this->thread_.terminate(); + } +} + bool StreamerModePrivate::isEnabled() const { this->timeouts_.store(SKIPPED_TIMEOUTS, std::memory_order::relaxed); diff --git a/src/singletons/StreamerMode.hpp b/src/singletons/StreamerMode.hpp index 5b7b6ef80fd..58ce43be6fa 100644 --- a/src/singletons/StreamerMode.hpp +++ b/src/singletons/StreamerMode.hpp @@ -20,6 +20,8 @@ class IStreamerMode : public QObject [[nodiscard]] virtual bool isEnabled() const = 0; + virtual void start() = 0; + signals: void changed(bool enabled); }; @@ -37,6 +39,8 @@ class StreamerMode : public IStreamerMode bool isEnabled() const override; + void start() override; + private: void updated(bool enabled); diff --git a/src/singletons/Updates.cpp b/src/singletons/Updates.cpp index 48d29946406..5d0b26e9d49 100644 --- a/src/singletons/Updates.cpp +++ b/src/singletons/Updates.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include namespace { @@ -40,9 +41,9 @@ const QString CHATTERINO_OS = u"freebsd"_s; #else const QString CHATTERINO_OS = u"unknown"_s; #endif -; } // namespace + namespace chatterino { Updates::Updates(const Paths &paths_) @@ -75,6 +76,26 @@ bool Updates::isDowngradeOf(const QString &online, const QString ¤t) return onlineVersion < currentVersion; } +void Updates::deleteOldFiles() +{ + std::ignore = QtConcurrent::run([dir{this->paths.miscDirectory}] { + { + auto path = combinePath(dir, "Update.exe"); + if (QFile::exists(path)) + { + QFile::remove(path); + } + } + { + auto path = combinePath(dir, "update.zip"); + if (QFile::exists(path)) + { + QFile::remove(path); + } + } + }); +} + const QString &Updates::getCurrentVersion() const { return currentVersion_; diff --git a/src/singletons/Updates.hpp b/src/singletons/Updates.hpp index 8a063f345ba..167bfab2be4 100644 --- a/src/singletons/Updates.hpp +++ b/src/singletons/Updates.hpp @@ -31,6 +31,11 @@ class Updates static bool isDowngradeOf(const QString &online, const QString ¤t); + /** + * @brief Delete old files that belong to the update process + */ + void deleteOldFiles(); + void checkForUpdates(); const QString &getCurrentVersion() const; const QString &getOnlineVersion() const; diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index cb5630c2520..07cee5d8b78 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -5,9 +5,6 @@ #include "common/QLogging.hpp" #include "debug/AssertInGuiThread.hpp" #include "messages/MessageElement.hpp" -#include "providers/irc/Irc2.hpp" -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" @@ -643,19 +640,6 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj) obj.insert("type", "live"); } break; - case Channel::Type::Irc: { - if (auto *ircChannel = - dynamic_cast(channel.get().get())) - { - obj.insert("type", "irc"); - if (ircChannel->server()) - { - obj.insert("server", ircChannel->server()->id()); - } - obj.insert("channel", ircChannel->getName()); - } - } - break; case Channel::Type::Misc: { obj.insert("type", "misc"); obj.insert("name", channel.get()->getName()); @@ -705,11 +689,6 @@ IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor) { return getApp()->getTwitch()->getAutomodChannel(); } - else if (descriptor.type_ == "irc") - { - return Irc::instance().getOrAddChannel(descriptor.server_, - descriptor.channelName_); - } else if (descriptor.type_ == "misc") { return getApp()->getTwitchAbstract()->getChannelOrEmpty( diff --git a/src/util/IpcQueue.cpp b/src/util/IpcQueue.cpp index 2e2e3df5496..397386974b1 100644 --- a/src/util/IpcQueue.cpp +++ b/src/util/IpcQueue.cpp @@ -101,6 +101,11 @@ std::pair, QString> IpcQueue::tryReplaceOrCreate( } } +bool IpcQueue::remove(const char *name) +{ + return boost_ipc::message_queue::remove(name); +} + QByteArray IpcQueue::receive() { try diff --git a/src/util/IpcQueue.hpp b/src/util/IpcQueue.hpp index e56cf2ba3df..059d15c6e9f 100644 --- a/src/util/IpcQueue.hpp +++ b/src/util/IpcQueue.hpp @@ -27,6 +27,8 @@ class IpcQueue static std::pair, QString> tryReplaceOrCreate( const char *name, size_t maxMessages, size_t maxMessageSize); + static bool remove(const char *name); + // TODO: use std::expected /// Try to receive a message. /// In the case of an error, the buffer is empty. diff --git a/src/util/RenameThread.cpp b/src/util/RenameThread.cpp new file mode 100644 index 00000000000..b4f1baea082 --- /dev/null +++ b/src/util/RenameThread.cpp @@ -0,0 +1,29 @@ +#include "util/RenameThread.hpp" + +#include "common/QLogging.hpp" + +#ifdef Q_OS_WIN + +# include + +namespace chatterino::windows::detail { + +void renameThread(HANDLE hThread, const QString &threadName) +{ +# if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + // SetThreadDescription requires Windows 10, version 1607 + // Qt 6 requires Windows 10 1809 + + auto hr = SetThreadDescription(hThread, threadName.toStdWString().c_str()); + if (!SUCCEEDED(hr)) + { + qCWarning(chatterinoCommon).nospace() + << "Failed to set thread description, hresult=0x" + << QString::number(hr, 16); + } +# endif +} + +} // namespace chatterino::windows::detail + +#endif diff --git a/src/util/RenameThread.hpp b/src/util/RenameThread.hpp new file mode 100644 index 00000000000..5212c8761a8 --- /dev/null +++ b/src/util/RenameThread.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#ifdef Q_OS_LINUX +# include +#endif + +namespace chatterino { + +#ifdef Q_OS_WIN +namespace windows::detail { + void renameThread(void *hThread, const QString &name); +} // namespace windows::detail +#endif + +template +void renameThread(T &thread, const QString &threadName) +{ +#ifdef Q_OS_LINUX + pthread_setname_np(thread.native_handle(), threadName.toLocal8Bit()); +#elif defined(Q_OS_WIN) + windows::detail::renameThread(thread.native_handle(), threadName); +#endif +} + +} // namespace chatterino diff --git a/src/util/StreamLink.cpp b/src/util/StreamLink.cpp index f9869ce4d8b..6bb6b6fc31f 100644 --- a/src/util/StreamLink.cpp +++ b/src/util/StreamLink.cpp @@ -3,7 +3,6 @@ #include "Application.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" -#include "providers/irc/IrcMessageBuilder.hpp" #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" #include "util/Helpers.hpp" diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 12e87ff73e1..7d24a85d4dd 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include @@ -503,6 +504,24 @@ bool BaseWindow::event(QEvent *event) this->onFocusLost(); } +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + if (this->flags_.has(DontFocus) || this->flags_.has(Dialog)) + { + // This certain windows (e.g. TooltipWidget, input completion widget, and the search popup) retains their nullptr parent + // NOTE that this currently does not retain their original transient parent (which is the window it was created under) + // For now, we haven't noticed that this creates any issues, and I don't know of a good place to store the previous transient + // parent to restore it. + if (event->type() == QEvent::ParentWindowChange) + { + assert(this->windowHandle() != nullptr); + if (this->windowHandle()->parent() != nullptr) + { + this->windowHandle()->setParent(nullptr); + } + } + } +#endif + return QWidget::event(event); } diff --git a/src/widgets/dialogs/IrcConnectionEditor.cpp b/src/widgets/dialogs/IrcConnectionEditor.cpp deleted file mode 100644 index 8ebfd8f9552..00000000000 --- a/src/widgets/dialogs/IrcConnectionEditor.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "widgets/dialogs/IrcConnectionEditor.hpp" - -#include "ui_IrcConnectionEditor.h" - -namespace chatterino { - -IrcConnectionEditor::IrcConnectionEditor(const IrcServerData &data, bool isAdd, - QWidget *parent) - - : QDialog(parent, Qt::WindowStaysOnTopHint) - , ui_(new Ui::IrcConnectionEditor) - , data_(data) -{ - this->ui_->setupUi(this); - - this->setWindowTitle(QString(isAdd ? "Add " : "Edit ") + "Irc Connection"); - - QObject::connect(this->ui_->userNameLineEdit, &QLineEdit::textChanged, this, - [this](const QString &text) { - this->ui_->nickNameLineEdit->setPlaceholderText(text); - this->ui_->realNameLineEdit->setPlaceholderText(text); - }); - - this->ui_->serverLineEdit->setText(data.host); - this->ui_->portSpinBox->setValue(data.port); - this->ui_->securityCheckBox->setChecked(data.ssl); - this->ui_->userNameLineEdit->setText(data.user); - this->ui_->nickNameLineEdit->setText(data.nick); - this->ui_->realNameLineEdit->setText(data.real); - this->ui_->connectCommandsEditor->setPlainText( - data.connectCommands.join('\n')); - - data.getPassword(this, [this](const QString &password) { - this->ui_->passwordLineEdit->setText(password); - }); - - this->ui_->loginMethodComboBox->setCurrentIndex([&] { - switch (data.authType) - { - case IrcAuthType::Custom: - return 1; - case IrcAuthType::Pass: - return 2; - case IrcAuthType::Sasl: - return 3; - default: - return 0; - } - }()); - - QObject::connect(this->ui_->loginMethodComboBox, - qOverload(&QComboBox::currentIndexChanged), this, - [this](int index) { - if (index == 1) // Custom - { - this->ui_->connectCommandsEditor->setFocus(); - } - }); - - QFont font("Monospace"); - font.setStyleHint(QFont::TypeWriter); - this->ui_->connectCommandsEditor->setFont(font); -} - -IrcConnectionEditor::~IrcConnectionEditor() -{ - delete ui_; -} - -IrcServerData IrcConnectionEditor::data() -{ - auto data = this->data_; - data.host = this->ui_->serverLineEdit->text(); - data.port = this->ui_->portSpinBox->value(); - data.ssl = this->ui_->securityCheckBox->isChecked(); - data.user = this->ui_->userNameLineEdit->text(); - data.nick = this->ui_->nickNameLineEdit->text(); - data.real = this->ui_->realNameLineEdit->text(); - data.connectCommands = - this->ui_->connectCommandsEditor->toPlainText().split('\n'); - data.setPassword(this->ui_->passwordLineEdit->text()); - data.authType = [this] { - switch (this->ui_->loginMethodComboBox->currentIndex()) - { - case 1: - return IrcAuthType::Custom; - case 2: - return IrcAuthType::Pass; - case 3: - return IrcAuthType::Sasl; - default: - return IrcAuthType::Anonymous; - } - }(); - return data; -} - -} // namespace chatterino diff --git a/src/widgets/dialogs/IrcConnectionEditor.hpp b/src/widgets/dialogs/IrcConnectionEditor.hpp deleted file mode 100644 index 9333f8830b8..00000000000 --- a/src/widgets/dialogs/IrcConnectionEditor.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include "providers/irc/Irc2.hpp" -#include "widgets/BaseWindow.hpp" - -#include - -namespace Ui { - -class IrcConnectionEditor; - -} // namespace Ui - -namespace chatterino { - -struct IrcServerData; - -class IrcConnectionEditor : public QDialog -{ - Q_OBJECT - -public: - explicit IrcConnectionEditor(const IrcServerData &data, bool isAdd = false, - QWidget *parent = nullptr); - IrcConnectionEditor(const IrcConnectionEditor &) = delete; - IrcConnectionEditor(IrcConnectionEditor &&) = delete; - IrcConnectionEditor &operator=(const IrcConnectionEditor &) = delete; - IrcConnectionEditor &operator=(IrcConnectionEditor &&) = delete; - ~IrcConnectionEditor() override; - - IrcServerData data(); - -private: - Ui::IrcConnectionEditor *ui_; - IrcServerData data_; -}; - -} // namespace chatterino diff --git a/src/widgets/dialogs/IrcConnectionEditor.ui b/src/widgets/dialogs/IrcConnectionEditor.ui deleted file mode 100644 index 516fe2b5548..00000000000 --- a/src/widgets/dialogs/IrcConnectionEditor.ui +++ /dev/null @@ -1,261 +0,0 @@ - - - IrcConnectionEditor - - - - 0 - 0 - 329 - 414 - - - - Dialog - - - - - - - - Host: - - - - - - - irc.example.com - - - - - - - Port: - - - - - - - 65636 - - - 6697 - - - - - - - SSL: - - - - - - - true - - - true - - - - - - - Qt::Vertical - - - - 20 - 20 - - - - - - - - User Name: - - - - - - - - - - Nick Name: - - - - - - - - - - Real Name: - - - - - - - - - - Login method: - - - - - - - Password: - - - - - - - QLineEdit::Password - - - - - - - Qt::Vertical - - - - 20 - 20 - - - - - - - - - Anonymous - - - - - Custom - - - - - Server Password (/PASS $password) - - - - - SASL - - - - - - - - Send IRC commands -on connect: - - - Qt::PlainText - - - - - - - - 0 - 1 - - - - - - - - Qt::Vertical - - - - 20 - 20 - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - serverLineEdit - portSpinBox - securityCheckBox - userNameLineEdit - nickNameLineEdit - realNameLineEdit - loginMethodComboBox - passwordLineEdit - - - - - buttonBox - accepted() - IrcConnectionEditor - accept() - - - 254 - 248 - - - 157 - 274 - - - - - buttonBox - rejected() - IrcConnectionEditor - reject() - - - 254 - 248 - - - 286 - 274 - - - - - diff --git a/src/widgets/dialogs/SelectChannelDialog.cpp b/src/widgets/dialogs/SelectChannelDialog.cpp index 728caac8b3f..559ebde0e31 100644 --- a/src/widgets/dialogs/SelectChannelDialog.cpp +++ b/src/widgets/dialogs/SelectChannelDialog.cpp @@ -3,14 +3,10 @@ #include "Application.hpp" #include "common/QLogging.hpp" #include "controllers/hotkeys/HotkeyController.hpp" -#include "providers/irc/Irc2.hpp" -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "util/LayoutCreator.hpp" -#include "widgets/dialogs/IrcConnectionEditor.hpp" #include "widgets/helper/EditableModelView.hpp" #include "widgets/helper/NotebookTab.hpp" #include "widgets/Notebook.hpp" @@ -28,7 +24,6 @@ namespace chatterino { constexpr int TAB_TWITCH = 0; -constexpr int TAB_IRC = 1; SelectChannelDialog::SelectChannelDialog(QWidget *parent) : BaseWindow( @@ -175,76 +170,6 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent) tab->setCustomTitle("Twitch"); } - // irc - { - LayoutCreator obj(new QWidget()); - auto outerBox = obj.setLayoutType(); - - { - auto *view = this->ui_.irc.servers = - new EditableModelView(Irc::instance().newConnectionModel(this)); - - view->setTitles({"host", "port", "ssl", "user", "nick", "real", - "password", "login command"}); - view->getTableView()->horizontalHeader()->resizeSection(0, 140); - - view->getTableView()->horizontalHeader()->setSectionHidden(1, true); - view->getTableView()->horizontalHeader()->setSectionHidden(2, true); - view->getTableView()->horizontalHeader()->setSectionHidden(4, true); - view->getTableView()->horizontalHeader()->setSectionHidden(5, true); - - // We can safely ignore this signal's connection since the button won't be - // accessible after this dialog is closed - std::ignore = view->addButtonPressed.connect([] { - auto unique = IrcServerData{}; - unique.id = Irc::instance().uniqueId(); - - auto *editor = new IrcConnectionEditor(unique); - if (editor->exec() == QDialog::Accepted) - { - Irc::instance().connections.append(editor->data()); - } - }); - - QObject::connect( - view->getTableView(), &QTableView::doubleClicked, - [](const QModelIndex &index) { - auto *editor = new IrcConnectionEditor( - Irc::instance().connections.raw()[size_t(index.row())]); - - if (editor->exec() == QDialog::Accepted) - { - auto data = editor->data(); - auto &&conns = Irc::instance().connections.raw(); - int i = 0; - for (auto &&conn : conns) - { - if (conn.id == data.id) - { - Irc::instance().connections.removeAt( - i, Irc::noEraseCredentialCaller); - Irc::instance().connections.insert(data, i); - } - i++; - } - } - }); - - outerBox->addRow("Server:", view); - } - - outerBox->addRow("Channel: #", this->ui_.irc.channel = new QLineEdit); - - auto *tab = notebook->addPage(obj.getElement()); - tab->setCustomTitle("Irc (Beta)"); - - if (!getSettings()->enableExperimentalIrc) - { - tab->setEnable(false); - tab->setVisible(false); - } - } - layout->setStretchFactor(notebook.getElement(), 1); auto buttons = @@ -265,29 +190,11 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent) this->ui_.notebook->selectIndex(TAB_TWITCH); this->ui_.twitch.channel->setFocus(); - // restore ui state - // fourtf: enable when releasing irc - if (getSettings()->enableExperimentalIrc) - { - this->ui_.notebook->selectIndex(getSettings()->lastSelectChannelTab); - } - this->addShortcuts(); - - this->ui_.irc.servers->getTableView()->selectRow( - getSettings()->lastSelectIrcConn); } void SelectChannelDialog::ok() { - // save ui state - getSettings()->lastSelectChannelTab = - this->ui_.notebook->getSelectedIndex(); - getSettings()->lastSelectIrcConn = this->ui_.irc.servers->getTableView() - ->selectionModel() - ->currentIndex() - .row(); - // accept and close this->hasSelectedChannel_ = true; this->close(); @@ -334,31 +241,6 @@ void SelectChannelDialog::setSelectedChannel(IndirectChannel _channel) this->ui_.twitch.automod->setFocus(); } break; - case Channel::Type::Irc: { - this->ui_.notebook->selectIndex(TAB_IRC); - this->ui_.irc.channel->setText(_channel.get()->getName()); - - if (auto *ircChannel = - dynamic_cast(_channel.get().get())) - { - if (auto *server = ircChannel->server()) - { - int i = 0; - for (auto &&conn : Irc::instance().connections) - { - if (conn.id == server->id()) - { - this->ui_.irc.servers->getTableView()->selectRow(i); - break; - } - i++; - } - } - } - - this->ui_.irc.channel->setFocus(); - } - break; default: { this->ui_.notebook->selectIndex(TAB_TWITCH); this->ui_.twitch.channel->setFocus(); @@ -405,25 +287,6 @@ IndirectChannel SelectChannelDialog::getSelectedChannel() const } } break; - case TAB_IRC: { - int row = this->ui_.irc.servers->getTableView() - ->selectionModel() - ->currentIndex() - .row(); - - auto &&vector = Irc::instance().connections.raw(); - - if (row >= 0 && row < int(vector.size())) - { - return Irc::instance().getOrAddChannel( - vector[size_t(row)].id, this->ui_.irc.channel->text()); - } - else - { - return Channel::getEmpty(); - } - } - //break; } return this->selectedChannel_; @@ -559,60 +422,9 @@ void SelectChannelDialog::addShortcuts() {"scrollPage", nullptr}, {"search", nullptr}, {"delete", nullptr}, + {"openTab", nullptr}, }; - if (getSettings()->enableExperimentalIrc) - { - actions.insert( - {"openTab", [this](std::vector arguments) -> QString { - if (arguments.size() == 0) - { - qCWarning(chatterinoHotkeys) - << "openTab shortcut called without arguments. " - "Takes only " - "one argument: tab specifier"; - return "openTab shortcut called without arguments. " - "Takes only one argument: tab specifier"; - } - auto target = arguments.at(0); - if (target == "last") - { - this->ui_.notebook->selectLastTab(); - } - else if (target == "next") - { - this->ui_.notebook->selectNextTab(); - } - else if (target == "previous") - { - this->ui_.notebook->selectPreviousTab(); - } - else - { - bool ok; - int result = target.toInt(&ok); - if (ok) - { - this->ui_.notebook->selectIndex(result); - } - else - { - qCWarning(chatterinoHotkeys) - << "Invalid argument for openTab shortcut"; - return QString("Invalid argument for openTab " - "shortcut: \"%1\". Use \"last\", " - "\"next\", \"previous\" or an integer.") - .arg(target); - } - } - return ""; - }}); - } - else - { - actions.emplace("openTab", nullptr); - } - this->shortcuts_ = getApp()->getHotkeys()->shortcutsForCategory( HotkeyCategory::PopupWindow, actions, this); } diff --git a/src/widgets/dialogs/SelectChannelDialog.hpp b/src/widgets/dialogs/SelectChannelDialog.hpp index 65b2cb56b18..4fb34b508ec 100644 --- a/src/widgets/dialogs/SelectChannelDialog.hpp +++ b/src/widgets/dialogs/SelectChannelDialog.hpp @@ -51,10 +51,6 @@ class SelectChannelDialog final : public BaseWindow QRadioButton *live; QRadioButton *automod; } twitch; - struct { - QLineEdit *channel; - EditableModelView *servers; - } irc; } ui_; EventFilter tabFilter_; diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index bef93ec7a3f..f1dbb687415 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -793,8 +793,7 @@ void UserInfoPopup::setData(const QString &name, auto type = this->channel_->getType(); if (type == Channel::Type::TwitchLive || - type == Channel::Type::TwitchWhispers || type == Channel::Type::Irc || - type == Channel::Type::Misc) + type == Channel::Type::TwitchWhispers || type == Channel::Type::Misc) { // not a normal twitch channel, the url opened by the button will be invalid, so hide the button this->ui_.usercardLabel->hide(); diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 522ab4ab2c1..9f410b9f9c4 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -2782,7 +2782,6 @@ bool ChannelView::mayContainMessage(const MessagePtr &message) case Channel::Type::Direct: case Channel::Type::Twitch: case Channel::Type::TwitchWatching: - case Channel::Type::Irc: // XXX: system messages may not have the channel set return message->flags.has(MessageFlag::System) || this->channel()->getName() == message->channelName; diff --git a/src/widgets/helper/ChannelView.hpp b/src/widgets/helper/ChannelView.hpp index a670d6e3074..f9f68bc2c1c 100644 --- a/src/widgets/helper/ChannelView.hpp +++ b/src/widgets/helper/ChannelView.hpp @@ -4,6 +4,7 @@ #include "messages/layouts/MessageLayoutContext.hpp" #include "messages/LimitedQueue.hpp" #include "messages/LimitedQueueSnapshot.hpp" +#include "messages/MessageFlag.hpp" #include "messages/Selection.hpp" #include "util/ThreadGuard.hpp" #include "widgets/BaseWidget.hpp" @@ -32,9 +33,6 @@ using ChannelPtr = std::shared_ptr; struct Message; using MessagePtr = std::shared_ptr; -enum class MessageFlag : int64_t; -using MessageFlags = FlagsEnum; - class MessageLayout; using MessageLayoutPtr = std::shared_ptr; diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 72cdb477db1..b823599837f 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -1142,13 +1142,6 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addIntInput("Usercard scrollback limit (requires restart)", s.scrollbackUsercardLimit, 100, 100000, 100); - layout.addCheckbox("Enable experimental IRC support (requires restart)", - s.enableExperimentalIrc, false, - "When enabled, attempting to join a channel will " - "include an \"IRC (Beta)\" tab allowing the user to " - "connect to an IRC server outside of Twitch "); - layout.addCheckbox("Show unhandled IRC messages", - s.showUnhandledIrcMessages); layout.addDropdown( "Stack timeouts", {"Stack", "Stack until timeout", "Don't stack"}, s.timeoutStackStyle, diff --git a/src/widgets/splits/SplitContainer.cpp b/src/widgets/splits/SplitContainer.cpp index 17047cb84cd..c2ddc1160b2 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -5,8 +5,6 @@ #include "common/QLogging.hpp" #include "common/WindowDescriptors.hpp" #include "debug/AssertInGuiThread.hpp" -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcServer.hpp" #include "singletons/Fonts.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" @@ -817,22 +815,6 @@ NodeDescriptor SplitContainer::buildDescriptorRecursively( SplitNodeDescriptor result; result.type_ = qmagicenum::enumNameString(channelType); - - switch (channelType) - { - case Channel::Type::Irc: { - if (auto *ircChannel = dynamic_cast( - currentNode->split_->getChannel().get())) - { - if (ircChannel->server()) - { - result.server_ = ircChannel->server()->id(); - } - } - } - break; - } - result.channelName_ = currentNode->split_->getChannel()->getName(); result.filters_ = currentNode->split_->getFilters(); return result; diff --git a/src/widgets/splits/SplitInput.cpp b/src/widgets/splits/SplitInput.cpp index 85c421be91f..b45928a82bb 100644 --- a/src/widgets/splits/SplitInput.cpp +++ b/src/widgets/splits/SplitInput.cpp @@ -50,12 +50,12 @@ SplitInput::SplitInput(QWidget *parent, Split *_chatWidget, this->initLayout(); auto *completer = - new QCompleter(&this->split_->getChannel()->completionModel); + new QCompleter(this->split_->getChannel()->completionModel); this->ui_.textEdit->setCompleter(completer); this->signalHolder_.managedConnect(this->split_->channelChanged, [this] { auto channel = this->split_->getChannel(); - auto *completer = new QCompleter(&channel->completionModel); + auto *completer = new QCompleter(channel->completionModel); this->ui_.textEdit->setCompleter(completer); }); diff --git a/tests/src/BasicPubSub.cpp b/tests/src/BasicPubSub.cpp index 6315970ff06..3b5c6b8f84f 100644 --- a/tests/src/BasicPubSub.cpp +++ b/tests/src/BasicPubSub.cpp @@ -68,7 +68,7 @@ class MyManager : public BasicPubSubManager { public: MyManager(QString host) - : BasicPubSubManager(std::move(host)) + : BasicPubSubManager(std::move(host), "Test") { } diff --git a/tests/src/Commands.cpp b/tests/src/Commands.cpp index 2a4d259aacd..420f71fdf5c 100644 --- a/tests/src/Commands.cpp +++ b/tests/src/Commands.cpp @@ -3,7 +3,7 @@ #include "controllers/commands/CommandContext.hpp" #include "controllers/commands/CommandController.hpp" #include "controllers/commands/common/ChannelAction.hpp" -#include "mocks/EmptyApplication.hpp" +#include "mocks/BaseApplication.hpp" #include "mocks/Helix.hpp" #include "mocks/Logging.hpp" #include "mocks/TwitchIrcServer.hpp" @@ -22,12 +22,11 @@ using ::testing::StrictMock; namespace { -class MockApplication : mock::EmptyApplication +class MockApplication : public mock::BaseApplication { public: MockApplication() - : settings(this->settingsDir.filePath("settings.json")) - , commands(this->paths_) + : commands(this->paths_) { } @@ -56,7 +55,6 @@ class MockApplication : mock::EmptyApplication return &this->chatLogger; } - Settings settings; AccountController accounts; CommandController commands; mock::MockTwitchIrcServer twitch; diff --git a/tests/src/FlagsEnum.cpp b/tests/src/FlagsEnum.cpp index 4b3e94ba534..0d165c9f026 100644 --- a/tests/src/FlagsEnum.cpp +++ b/tests/src/FlagsEnum.cpp @@ -326,6 +326,23 @@ TEST(FlagsEnum, hasNone) testHasNone(); } +template +consteval void testIsEmpty() +{ + using FE = FlagsEnum; + + static_assert(FE{}.isEmpty()); + static_assert(!FE{E::Foo}.isEmpty()); + static_assert(FE{E::None}.isEmpty()); + static_assert(!FE{E::Foo, E::Waldo}.isEmpty()); +} + +TEST(FlagsEnum, isEmpty) +{ + testIsEmpty(); + testIsEmpty(); +} + template constexpr inline auto CONSTRUCTION_VALID = requires() { FlagsEnum{}; };