From 7d4ea79376862da0946af86858c41e1de7f0eadd Mon Sep 17 00:00:00 2001 From: nerix Date: Tue, 16 Dec 2025 11:06:15 +0100 Subject: [PATCH] refactor: move Twitch PubSub to use liveupdates (#6638) Co-authored-by: Rasmus Karlsson --- .clang-tidy | 2 + .codecov.yml | 1 - .gitmodules | 4 - CHANGELOG.md | 1 + CMakeLists.txt | 2 - cmake/FindWebsocketpp.cmake | 14 - lib/websocketpp | 1 - resources/licenses/websocketpp.txt | 145 ----- src/Application.cpp | 1 - src/CMakeLists.txt | 5 - .../NetworkConfigurationProvider.hpp | 35 -- .../bttv/liveupdates/BttvLiveUpdateClient.cpp | 2 +- .../bttv/liveupdates/BttvLiveUpdateClient.hpp | 2 +- .../liveupdates/BasicPubSubClient.hpp | 18 +- src/providers/seventv/eventapi/Client.cpp | 4 +- src/providers/seventv/eventapi/Client.hpp | 4 +- .../twitch/ChatterinoWebSocketppLogger.hpp | 194 ------ src/providers/twitch/PubSubClient.cpp | 308 +++++----- src/providers/twitch/PubSubClient.hpp | 88 +-- src/providers/twitch/PubSubClientOptions.hpp | 14 - src/providers/twitch/PubSubHelpers.hpp | 53 -- src/providers/twitch/PubSubManager.cpp | 581 ++---------------- src/providers/twitch/PubSubManager.hpp | 111 +--- src/providers/twitch/PubSubWebsocket.hpp | 32 - src/providers/twitch/pubsubmessages/Base.cpp | 4 +- src/providers/twitch/pubsubmessages/Base.hpp | 2 +- src/util/SampleData.cpp | 16 +- src/util/SampleData.hpp | 4 +- src/widgets/settingspages/AboutPage.cpp | 3 - tests/src/BasicPubSub.cpp | 2 +- tests/src/TwitchPubSubClient.cpp | 163 +++-- 31 files changed, 353 insertions(+), 1463 deletions(-) delete mode 100644 cmake/FindWebsocketpp.cmake delete mode 160000 lib/websocketpp delete mode 100644 resources/licenses/websocketpp.txt delete mode 100644 src/providers/twitch/ChatterinoWebSocketppLogger.hpp delete mode 100644 src/providers/twitch/PubSubClientOptions.hpp delete mode 100644 src/providers/twitch/PubSubHelpers.hpp delete mode 100644 src/providers/twitch/PubSubWebsocket.hpp diff --git a/.clang-tidy b/.clang-tidy index 88ae90c1..f2df52ac 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -15,6 +15,8 @@ Checks: "-*, cert-*, clazy-*, -clazy-qstring-allocations, + -clazy-missing-qobject-macro, + -clazy-ctor-missing-parent-argument, cppcoreguidelines-*, -cppcoreguidelines-pro-type-cstyle-cast, -cppcoreguidelines-pro-type-vararg, diff --git a/.codecov.yml b/.codecov.yml index 705e496a..3db72030 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -17,7 +17,6 @@ ignore: - "**/lib/settings/*" - "**/lib/signals/*" - "**/lib/sol2/*" - - "**/lib/websocketpp/*" - "**/lib/WinToast/*" - "**/build*/" - "**/ui_*.h" diff --git a/.gitmodules b/.gitmodules index bd42da3a..d1a5352b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -20,10 +20,6 @@ [submodule "lib/qtkeychain"] path = lib/qtkeychain url = https://github.com/frankosterfeld/qtkeychain -[submodule "lib/websocketpp"] - path = lib/websocketpp - url = https://github.com/Chatterino/websocketpp - branch = chatterino [submodule "cmake/sanitizers-cmake"] path = cmake/sanitizers-cmake url = https://github.com/arsenm/sanitizers-cmake diff --git a/CHANGELOG.md b/CHANGELOG.md index b51ec9c0..2beb4b51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ - Dev: Disabled `llvm-prefer-static-over-anonymous-namespace` in clang-tidy. (#6610) - Dev: Added Clazy linting in CI. (#6623) - Dev: Added custom clang-tidy module linting in CI. (#6626) +- Dev: Moved Twitch PubSub to liveupdates. (#6638) ## 2.5.4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 869c994a..38a2cbfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,8 +197,6 @@ if (BUILD_WITH_QTKEYCHAIN) endif() endif() -find_package(Websocketpp REQUIRED) - if (BUILD_TESTS) include(GoogleTest) # For MSVC: Prevent overriding the parent project's compiler/linker settings diff --git a/cmake/FindWebsocketpp.cmake b/cmake/FindWebsocketpp.cmake deleted file mode 100644 index f8d0bb65..00000000 --- a/cmake/FindWebsocketpp.cmake +++ /dev/null @@ -1,14 +0,0 @@ -include(FindPackageHandleStandardArgs) - -find_path(Websocketpp_INCLUDE_DIR websocketpp/version.hpp HINTS ${CMAKE_SOURCE_DIR}/lib/websocketpp) - -find_package_handle_standard_args(Websocketpp DEFAULT_MSG Websocketpp_INCLUDE_DIR) - -if (Websocketpp_FOUND) - add_library(websocketpp::websocketpp INTERFACE IMPORTED) - set_target_properties(websocketpp::websocketpp PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${Websocketpp_INCLUDE_DIR}" - ) -endif () - -mark_as_advanced(Websocketpp_INCLUDE_DIR) diff --git a/lib/websocketpp b/lib/websocketpp deleted file mode 160000 index f1736a8e..00000000 --- a/lib/websocketpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f1736a8e72b910810ff6869fe20f647a62f3bc35 diff --git a/resources/licenses/websocketpp.txt b/resources/licenses/websocketpp.txt deleted file mode 100644 index f8cc5ba1..00000000 --- a/resources/licenses/websocketpp.txt +++ /dev/null @@ -1,145 +0,0 @@ -Main Library: - -Copyright (c) 2014, Peter Thorson. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the WebSocket++ Project nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL PETER THORSON BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -Bundled Libraries: - -****** Base 64 Library (base64/base64.hpp) ****** -base64.hpp is a repackaging of the base64.cpp and base64.h files into a -single header suitable for use as a header only library. This conversion was -done by Peter Thorson (webmaster@zaphoyd.com) in 2012. All modifications to -the code are redistributed under the same license as the original, which is -listed below. - -base64.cpp and base64.h - -Copyright (C) 2004-2008 René Nyffenegger - -This source code is provided 'as-is', without any express or implied -warranty. In no event will the author be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - -1. The origin of this source code must not be misrepresented; you must not - claim that you wrote the original source code. If you use this source code - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original source code. - -3. This notice may not be removed or altered from any source distribution. - -René Nyffenegger rene.nyffenegger@adp-gmbh.ch - -****** SHA1 Library (sha1/sha1.hpp) ****** -sha1.hpp is a repackaging of the sha1.cpp and sha1.h files from the shallsha1 -library (http://code.google.com/p/smallsha1/) into a single header suitable for -use as a header only library. This conversion was done by Peter Thorson -(webmaster@zaphoyd.com) in 2013. All modifications to the code are redistributed -under the same license as the original, which is listed below. - - Copyright (c) 2011, Micael Hildenborg - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Micael Hildenborg nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY Micael Hildenborg ''AS IS'' AND ANY - EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL Micael Hildenborg BE LIABLE FOR ANY - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -****** MD5 Library (common/md5.hpp) ****** -md5.hpp is a reformulation of the md5.h and md5.c code from -http://www.opensource.apple.com/source/cups/cups-59/cups/md5.c to allow it to -function as a component of a header only library. This conversion was done by -Peter Thorson (webmaster@zaphoyd.com) in 2012 for the WebSocket++ project. The -changes are released under the same license as the original (listed below) - -Copyright (C) 1999, 2002 Aladdin Enterprises. All rights reserved. - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. - -L. Peter Deutsch -ghost@aladdin.com - -****** UTF8 Validation logic (utf8_validation.hpp) ****** -utf8_validation.hpp is adapted from code originally written by Bjoern Hoehrmann -. See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for -details. - -The original license: - -Copyright (c) 2008-2009 Bjoern Hoehrmann - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/Application.cpp b/src/Application.cpp index 7d2b70d6..0427774f 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -291,7 +291,6 @@ void Application::initialize(Settings &settings, const Paths &paths) { this->initNm(paths); } - this->twitchPubSub->initialize(); this->twitch->initEventAPIs(this->bttvLiveUpdates.get(), this->seventvEventAPI.get()); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 72b110e4..4f6e88b1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -433,12 +433,9 @@ set(SOURCE_FILES providers/twitch/IrcMessageHandler.hpp providers/twitch/PubSubClient.cpp providers/twitch/PubSubClient.hpp - providers/twitch/PubSubClientOptions.hpp - providers/twitch/PubSubHelpers.hpp providers/twitch/PubSubManager.cpp providers/twitch/PubSubManager.hpp providers/twitch/PubSubMessages.hpp - providers/twitch/PubSubWebsocket.hpp providers/twitch/TwitchAccount.cpp providers/twitch/TwitchAccount.hpp providers/twitch/TwitchAccountManager.cpp @@ -886,7 +883,6 @@ if (CHATTERINO_GENERATE_COVERAGE) EXCLUDE "lib/settings/*" EXCLUDE "lib/signals/*" EXCLUDE "lib/sol2/*" - EXCLUDE "lib/websocketpp/*" EXCLUDE "lib/WinToast/*" EXCLUDE "*/ui_*.h" @@ -918,7 +914,6 @@ target_link_libraries(${LIBRARY_PROJECT} Pajlada::Serialize Pajlada::Settings Pajlada::Signals - websocketpp::websocketpp Threads::Threads LRUCache MagicEnum diff --git a/src/providers/NetworkConfigurationProvider.hpp b/src/providers/NetworkConfigurationProvider.hpp index 6e277cfd..9baf0a25 100644 --- a/src/providers/NetworkConfigurationProvider.hpp +++ b/src/providers/NetworkConfigurationProvider.hpp @@ -1,11 +1,6 @@ #pragma once -#include "common/QLogging.hpp" - #include -#include - -#include namespace chatterino { @@ -24,36 +19,6 @@ public: * Currently a proxy is applied if configured. */ static void applyFromEnv(const Env &env); - - static void applyToWebSocket(const auto &connection) - { - const auto applicationProxy = QNetworkProxy::applicationProxy(); - if (applicationProxy.type() != QNetworkProxy::HttpProxy) - { - return; - } - std::string url = "http://"; - url += applicationProxy.hostName().toStdString(); - url += ":"; - url += std::to_string(applicationProxy.port()); - websocketpp::lib::error_code ec; - connection->set_proxy(url, ec); - if (ec) - { - qCDebug(chatterinoNetwork) - << "Couldn't set websocket proxy:" << ec.value(); - return; - } - - connection->set_proxy_basic_auth( - applicationProxy.user().toStdString(), - applicationProxy.password().toStdString(), ec); - if (ec) - { - qCDebug(chatterinoNetwork) - << "Couldn't set websocket proxy auth:" << ec.value(); - } - } }; } // namespace chatterino diff --git a/src/providers/bttv/liveupdates/BttvLiveUpdateClient.cpp b/src/providers/bttv/liveupdates/BttvLiveUpdateClient.cpp index 10f255ef..0cd314e0 100644 --- a/src/providers/bttv/liveupdates/BttvLiveUpdateClient.cpp +++ b/src/providers/bttv/liveupdates/BttvLiveUpdateClient.cpp @@ -13,7 +13,7 @@ namespace chatterino { BttvLiveUpdateClient::BttvLiveUpdateClient(BttvLiveUpdates &manager) - : BasicPubSubClient(100) + : BasicPubSubClient(100) , manager(manager) { } diff --git a/src/providers/bttv/liveupdates/BttvLiveUpdateClient.hpp b/src/providers/bttv/liveupdates/BttvLiveUpdateClient.hpp index e3b1e06e..ef9259d6 100644 --- a/src/providers/bttv/liveupdates/BttvLiveUpdateClient.hpp +++ b/src/providers/bttv/liveupdates/BttvLiveUpdateClient.hpp @@ -8,7 +8,7 @@ namespace chatterino { class BttvLiveUpdates; class BttvLiveUpdateClient - : public BasicPubSubClient + : public BasicPubSubClient { public: BttvLiveUpdateClient(BttvLiveUpdates &manager); diff --git a/src/providers/liveupdates/BasicPubSubClient.hpp b/src/providers/liveupdates/BasicPubSubClient.hpp index 6f15b23e..641ec6c2 100644 --- a/src/providers/liveupdates/BasicPubSubClient.hpp +++ b/src/providers/liveupdates/BasicPubSubClient.hpp @@ -20,7 +20,7 @@ namespace chatterino { * * @tparam Subscription see BasicPubSubManager */ -template +template class BasicPubSubClient { public: @@ -66,6 +66,16 @@ public: this->ws_.sendText(data); } + QByteArray encodeSubscription(const Subscription &subscription) + { + return subscription.encodeSubscribe(); + } + + QByteArray encodeUnsubscription(const Subscription &subscription) + { + return subscription.encodeUnsubscribe(); + } + protected: /** * @return true if this client subscribed to this subscription @@ -93,7 +103,8 @@ protected: qCDebug(chatterinoLiveupdates) << "Subscribing to" << subscription; DebugCount::increase("LiveUpdates subscriptions"); - QByteArray encoded = subscription.encodeSubscribe(); + QByteArray encoded = + static_cast(this)->encodeSubscription(subscription); this->ws_.sendText(encoded); return true; @@ -113,7 +124,8 @@ protected: qCDebug(chatterinoLiveupdates) << "Unsubscribing from" << subscription; DebugCount::decrease("LiveUpdates subscriptions"); - QByteArray encoded = subscription.encodeUnsubscribe(); + QByteArray encoded = + static_cast(this)->encodeUnsubscription(subscription); this->ws_.sendText(encoded); return true; diff --git a/src/providers/seventv/eventapi/Client.cpp b/src/providers/seventv/eventapi/Client.cpp index 6382b35c..d2b87f5c 100644 --- a/src/providers/seventv/eventapi/Client.cpp +++ b/src/providers/seventv/eventapi/Client.cpp @@ -14,7 +14,7 @@ namespace chatterino::seventv::eventapi { Client::Client(SeventvEventAPI &manager, std::chrono::milliseconds heartbeatInterval) - : BasicPubSubClient(100) + : BasicPubSubClient(100) , lastHeartbeat_(std::chrono::steady_clock::now()) , heartbeatInterval_(heartbeatInterval) , manager_(manager) @@ -23,7 +23,7 @@ Client::Client(SeventvEventAPI &manager, void Client::onOpen() { - BasicPubSubClient::onOpen(); + BasicPubSubClient::onOpen(); this->lastHeartbeat_.store(std::chrono::steady_clock::now(), std::memory_order::relaxed); } diff --git a/src/providers/seventv/eventapi/Client.hpp b/src/providers/seventv/eventapi/Client.hpp index dad66758..b12896f7 100644 --- a/src/providers/seventv/eventapi/Client.hpp +++ b/src/providers/seventv/eventapi/Client.hpp @@ -18,8 +18,8 @@ struct Dispatch; struct CosmeticCreateDispatch; struct EntitlementCreateDeleteDispatch; -class Client : public BasicPubSubClient, - std::enable_shared_from_this +class Client : public BasicPubSubClient, + public std::enable_shared_from_this { public: Client(SeventvEventAPI &manager, diff --git a/src/providers/twitch/ChatterinoWebSocketppLogger.hpp b/src/providers/twitch/ChatterinoWebSocketppLogger.hpp deleted file mode 100644 index c846affa..00000000 --- a/src/providers/twitch/ChatterinoWebSocketppLogger.hpp +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (c) 2020, Peter Thorson, Steve Wills. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * * Neither the name of the WebSocket++ Project nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL PETER THORSON BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -#pragma once - -#include "common/QLogging.hpp" - -#include -#include -#include - -#include - -namespace websocketpp::log { - -template -class chatterinowebsocketpplogger : public basic -{ -public: - using base = chatterinowebsocketpplogger; - - chatterinowebsocketpplogger(channel_type_hint::value) - : m_static_channels(0xffffffff) - , m_dynamic_channels(0) - { - } - - chatterinowebsocketpplogger(std::ostream *) - : m_static_channels(0xffffffff) - , m_dynamic_channels(0) - { - } - - chatterinowebsocketpplogger(level c, channel_type_hint::value) - : m_static_channels(c) - , m_dynamic_channels(0) - { - } - - chatterinowebsocketpplogger(level c, std::ostream *) - : m_static_channels(c) - , m_dynamic_channels(0) - { - } - - ~chatterinowebsocketpplogger() - { - } - - chatterinowebsocketpplogger( - chatterinowebsocketpplogger const &other) - : m_static_channels(other.m_static_channels) - , m_dynamic_channels(other.m_dynamic_channels) - { - } - -#ifdef _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ - chatterinowebsocketpplogger &operator=( - chatterinowebsocketpplogger const &) = delete; -#endif // _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ - -#ifdef _WEBSOCKETPP_MOVE_SEMANTICS_ - /// Move constructor - chatterinowebsocketpplogger( - chatterinowebsocketpplogger &&other) - : m_static_channels(other.m_static_channels) - , m_dynamic_channels(other.m_dynamic_channels) - { - } -# ifdef _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ - // no move assignment operator because of const member variables - chatterinowebsocketpplogger &operator=( - chatterinowebsocketpplogger &&) = delete; -# endif // _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ -#endif // _WEBSOCKETPP_MOVE_SEMANTICS_ - - /// Explicitly do nothing, this logger doesn't support changing ostream - void set_ostream(std::ostream *) - { - } - - /// Dynamically enable the given list of channels - /** - * @param channels The package of channels to enable - */ - void set_channels(level channels) - { - if (channels == names::none) - { - clear_channels(names::all); - return; - } - - scoped_lock_type lock(m_lock); - m_dynamic_channels |= (channels & m_static_channels); - } - - /// Dynamically disable the given list of channels - /** - * @param channels The package of channels to disable - */ - void clear_channels(level channels) - { - scoped_lock_type lock(m_lock); - m_dynamic_channels &= ~channels; - } - - /// Write a string message to the given channel - /** - * @param channel The channel to write to - * @param msg The message to write - */ - void write(level channel, std::string const &msg) - { - scoped_lock_type lock(m_lock); - if (!this->dynamic_test(channel)) - { - return; - } - qCDebug(chatterinoWebsocket).nospace() - << names::channel_name(channel) << ": " - << QString::fromStdString(msg); - } - - /// Write a cstring message to the given channel - /** - * @param channel The channel to write to - * @param msg The message to write - */ - void write(level channel, char const *msg) - { - scoped_lock_type lock(m_lock); - if (!this->dynamic_test(channel)) - { - return; - } - qCDebug(chatterinoWebsocket).nospace() - << names::channel_name(channel) << ": " << msg; - } - - /// Test whether a channel is statically enabled - /** - * @param channel The package of channels to test - */ - - _WEBSOCKETPP_CONSTEXPR_TOKEN_ bool static_test(level channel) const - { - return ((channel & m_static_channels) != 0); - } - - /// Test whether a channel is dynamically enabled - /** - * @param channel The package of channels to test - */ - bool dynamic_test(level channel) - { - return ((channel & m_dynamic_channels) != 0); - } - -protected: - using scoped_lock_type = typename concurrency::scoped_lock_type; - using mutex_type = typename concurrency::mutex_type; - mutex_type m_lock; - -private: - level const m_static_channels; - level m_dynamic_channels; -}; - -} // namespace websocketpp::log diff --git a/src/providers/twitch/PubSubClient.cpp b/src/providers/twitch/PubSubClient.cpp index 0e0860b4..19f9d537 100644 --- a/src/providers/twitch/PubSubClient.cpp +++ b/src/providers/twitch/PubSubClient.cpp @@ -1,224 +1,196 @@ #include "providers/twitch/PubSubClient.hpp" #include "common/QLogging.hpp" -#include "providers/twitch/PubSubHelpers.hpp" +#include "providers/twitch/PubSubManager.hpp" #include "providers/twitch/PubSubMessages.hpp" -#include "util/DebugCount.hpp" namespace chatterino { -static const char *PING_PAYLOAD = R"({"type":"PING"})"; +using namespace Qt::Literals; -PubSubClient::PubSubClient(WebsocketClient &websocketClient, - WebsocketHandle handle, - const PubSubClientOptions &clientOptions) - : websocketClient_(websocketClient) - , handle_(handle) - , heartbeatTimer_(std::make_shared( - this->websocketClient_.get_io_service())) - , clientOptions_(clientOptions) +QDebug operator<<(QDebug debug, const TopicData &data) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "TopicData(" << data.topic << ')'; + + return debug; +} + +PubSubClient::PubSubClient(PubSub &manager, + std::chrono::milliseconds heartbeatInterval) + : BasicPubSubClient(MAX_LISTENS) + , heartbeatInterval_(heartbeatInterval) + , manager_(manager) { } -void PubSubClient::start() +void PubSubClient::onOpen() { - assert(!this->started_); - - this->started_ = true; - - this->ping(); + BasicPubSubClient::onOpen(); + this->isOpen_ = true; + this->lastHeartbeat_ = std::chrono::steady_clock::now(); } -void PubSubClient::stop() +void PubSubClient::onMessage(const QByteArray &msg) { - assert(this->started_); + this->manager_.diag.messagesReceived++; - this->started_ = false; - this->heartbeatTimer_->cancel(); -} + auto optMessage = parsePubSubBaseMessage(msg); + if (!optMessage) + { + qCDebug(chatterinoPubSub) + << "Unable to parse incoming pubsub message" << msg; + this->manager_.diag.messagesFailedToParse += 1; + return; + } -void PubSubClient::close(const std::string &reason, - websocketpp::close::status::value code) -{ - boost::asio::post( - this->websocketClient_.get_io_service().get_executor(), - [this, reason, code] { - // We need to post this request to the io service executor - // to ensure the weak pointer used in get_con_from_hdl is used in a safe way - WebsocketErrorCode ec; + auto message = *optMessage; - auto conn = - this->websocketClient_.get_con_from_hdl(this->handle_, ec); - if (ec) + switch (message.type) + { + case PubSubMessage::Type::Pong: { + this->lastHeartbeat_.store(std::chrono::steady_clock::now()); + } + break; + + case PubSubMessage::Type::Response: { + this->handleResponse(message); + } + break; + + case PubSubMessage::Type::Message: { + auto oMessageMessage = message.toInner(); + if (!oMessageMessage) { - qCDebug(chatterinoPubSub) - << "Error getting con:" << ec.message().c_str(); + qCDebug(chatterinoPubSub) << "Malformed MESSAGE:" << msg; return; } - conn->close(code, reason, ec); - if (ec) - { - qCDebug(chatterinoPubSub) - << "Error closing:" << ec.message().c_str(); - return; - } - }); + this->handleMessageResponse(*oMessageMessage); + } + break; + + case PubSubMessage::Type::INVALID: + default: { + qCDebug(chatterinoPubSub) + << "Unknown message type:" << message.typeString; + } + break; + } } -bool PubSubClient::listen(const PubSubListenMessage &msg) +void PubSubClient::checkHeartbeat() { - auto numRequestedListens = msg.topics.size(); - - if (this->numListens_ + numRequestedListens > PubSubClient::MAX_LISTENS) + if (!this->isOpen_) { - // This PubSubClient is already at its peak listens - return false; - } - this->numListens_ += numRequestedListens; - DebugCount::increase("PubSub topic pending listens", - static_cast(numRequestedListens)); - - for (const auto &topic : msg.topics) - { - this->listeners_.emplace_back(Listener{ - TopicData{ - topic, - false, - false, - }, - false, - }); + return; } - qCDebug(chatterinoPubSub) - << "Subscribing to" << numRequestedListens << "topics"; + auto dur = std::chrono::steady_clock::now() - this->lastHeartbeat_.load(); + if (dur > this->heartbeatInterval_ * 1.5) + { + qCDebug(chatterinoPubSub) << "Heartbeat timed out"; + this->close(); + } - this->send(msg.toJson()); - - return true; + this->sendText(R"({"type":"PING"})"_ba); } -PubSubClient::UnlistenPrefixResponse PubSubClient::unlistenPrefix( - const QString &prefix) +QByteArray PubSubClient::encodeSubscription(const Subscription &subscription) { - std::vector topics; + PubSubListenMessage listen({subscription.topic}); + this->nonces_[listen.nonce] = NonceInfo{ + .isListen = true, + }; + return listen.toJson(); +} - for (auto it = this->listeners_.begin(); it != this->listeners_.end();) +QByteArray PubSubClient::encodeUnsubscription(const Subscription &subscription) +{ + PubSubUnlistenMessage unlisten({subscription.topic}); + this->nonces_[unlisten.nonce] = NonceInfo{ + .isListen = true, + }; + return unlisten.toJson(); +} + +void PubSubClient::handleResponse(const PubSubMessage &message) +{ + const bool failed = !message.error.isEmpty(); + + if (failed) { - const auto &listener = *it; - if (listener.topic.startsWith(prefix)) + qCDebug(chatterinoPubSub) + << "Error" << message.error << "on nonce" << message.nonce; + } + + if (message.nonce.isEmpty()) + { + // Can't do any specific handling since no nonce was specified + return; + } + + auto nonceInfoIt = this->nonces_.find(message.nonce); + if (nonceInfoIt == this->nonces_.end()) + { + qCDebug(chatterinoPubSub) << "Unknown nonce:" << message.nonce; + return; + } + + if (nonceInfoIt->second.isListen) + { + if (failed) { - topics.push_back(listener.topic); - it = this->listeners_.erase(it); + this->manager_.diag.failedListenResponses++; } else { - ++it; + this->manager_.diag.listenResponses++; } } - - if (topics.empty()) + else { - return {{}, ""}; + this->manager_.diag.unlistenResponses++; } - auto numRequestedUnlistens = topics.size(); - - this->numListens_ -= numRequestedUnlistens; - DebugCount::increase("PubSub topic pending unlistens", - static_cast(numRequestedUnlistens)); - - PubSubUnlistenMessage message(topics); - - this->send(message.toJson()); - - return {message.topics, message.nonce}; + this->nonces_.erase(nonceInfoIt); } -void PubSubClient::handleListenResponse(const PubSubMessage &message) +void PubSubClient::handleMessageResponse(const PubSubMessageMessage &message) { -} - -void PubSubClient::handleUnlistenResponse(const PubSubMessage &message) -{ -} - -void PubSubClient::handlePong() -{ - assert(this->awaitingPong_); - - this->awaitingPong_ = false; -} - -bool PubSubClient::isListeningToTopic(const QString &topic) -{ - for (const auto &listener : this->listeners_) - { - if (listener.topic == topic) - { - return true; - } - } - - return false; -} - -std::vector PubSubClient::getListeners() const -{ - return this->listeners_; -} - -void PubSubClient::ping() -{ - assert(this->started_); - - if (this->awaitingPong_) - { - qCDebug(chatterinoPubSub) << "No pong response, disconnect!"; - this->close("Didn't respond to ping"); - - return; - } - - if (!this->send(PING_PAYLOAD)) + if (!message.topic.startsWith("community-points-channel-v1.")) { return; } - this->awaitingPong_ = true; - - auto self = this->shared_from_this(); - - runAfter(this->heartbeatTimer_, this->clientOptions_.pingInterval_, - [self](auto timer) { - (void)timer; - if (!self->started_) - { - return; - } - - self->ping(); - }); -} - -bool PubSubClient::send(const char *payload) -{ - WebsocketErrorCode ec; - this->websocketClient_.send(this->handle_, payload, - websocketpp::frame::opcode::text, ec); - - if (ec) + auto oInnerMessage = + message.toInner(); + if (!oInnerMessage) { - qCDebug(chatterinoPubSub) << "Error sending message" << payload << ":" - << ec.message().c_str(); - // TODO(pajlada): Check which error code happened and maybe - // gracefully handle it - - return false; + qCDebug(chatterinoPubSub) + << "Malformed community-points-channel-v1 message"; + return; } - return true; + const auto &innerMessage = *oInnerMessage; + + switch (innerMessage.type) + { + case PubSubCommunityPointsChannelV1Message::Type:: + AutomaticRewardRedeemed: + case PubSubCommunityPointsChannelV1Message::Type::RewardRedeemed: { + auto redemption = innerMessage.data.value("redemption").toObject(); + this->manager_.pointReward.redeemed.invoke(redemption); + } + break; + + case PubSubCommunityPointsChannelV1Message::Type::INVALID: + default: { + qCDebug(chatterinoPubSub) + << "Invalid point event type:" << innerMessage.typeString; + } + break; + } } } // namespace chatterino diff --git a/src/providers/twitch/PubSubClient.hpp b/src/providers/twitch/PubSubClient.hpp index 01d212de..f74786aa 100644 --- a/src/providers/twitch/PubSubClient.hpp +++ b/src/providers/twitch/PubSubClient.hpp @@ -1,7 +1,7 @@ #pragma once -#include "providers/twitch/PubSubClientOptions.hpp" -#include "providers/twitch/PubSubWebsocket.hpp" +#include "providers/liveupdates/BasicPubSubClient.hpp" +#include "providers/twitch/pubsubmessages/Message.hpp" #include #include @@ -11,67 +11,71 @@ namespace chatterino { -struct PubSubMessage; -struct PubSubListenMessage; - struct TopicData { QString topic; - bool authed{false}; - bool persistent{false}; + + bool operator==(const TopicData &other) const + { + return this->topic == other.topic; + } + + friend QDebug operator<<(QDebug debug, const TopicData &data); }; -struct Listener : TopicData { - bool confirmed{false}; +} // namespace chatterino + +template <> +struct std::hash { + std::size_t operator()(const chatterino::TopicData &data) const noexcept + { + return std::hash{}(data.topic); + } }; -class PubSubClient : public std::enable_shared_from_this +namespace chatterino { + +struct PubSubMessage; +class PubSub; +struct PubSubListenMessage; + +class PubSubClient : public BasicPubSubClient { public: + // The max amount of topics we may listen to with a single connection + static constexpr size_t MAX_LISTENS = 50; + struct UnlistenPrefixResponse { std::vector topics; QString nonce; }; - // The max amount of topics we may listen to with a single connection - static constexpr std::vector::size_type MAX_LISTENS = 50; + PubSubClient(PubSub &manager, std::chrono::milliseconds heartbeatInterval); - PubSubClient(WebsocketClient &_websocketClient, WebsocketHandle _handle, - const PubSubClientOptions &clientOptions); + void onOpen() /* override */; + void onMessage(const QByteArray &msg) /* override */; - void start(); - void stop(); + void checkHeartbeat(); - void close(const std::string &reason, - websocketpp::close::status::value code = - websocketpp::close::status::normal); - - bool listen(const PubSubListenMessage &msg); - UnlistenPrefixResponse unlistenPrefix(const QString &prefix); - - void handleListenResponse(const PubSubMessage &message); - void handleUnlistenResponse(const PubSubMessage &message); - - void handlePong(); - - bool isListeningToTopic(const QString &topic); - - std::vector getListeners() const; + QByteArray encodeSubscription( + const Subscription &subscription) /* override */; + QByteArray encodeUnsubscription( + const Subscription &subscription) /* override */; private: - void ping(); - bool send(const char *payload); + struct NonceInfo { + bool isListen; + }; - WebsocketClient &websocketClient_; - WebsocketHandle handle_; - uint16_t numListens_ = 0; + void handleResponse(const PubSubMessage &message); + void handleMessageResponse(const PubSubMessageMessage &message); - std::vector listeners_; + std::unordered_map nonces_; - std::atomic awaitingPong_{false}; - std::atomic started_{false}; - - std::shared_ptr heartbeatTimer_; - const PubSubClientOptions &clientOptions_; + std::atomic> + lastHeartbeat_; + std::chrono::milliseconds heartbeatInterval_; + bool isOpen_ = false; + PubSub &manager_; }; } // namespace chatterino diff --git a/src/providers/twitch/PubSubClientOptions.hpp b/src/providers/twitch/PubSubClientOptions.hpp deleted file mode 100644 index 2bd576ef..00000000 --- a/src/providers/twitch/PubSubClientOptions.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include - -namespace chatterino { - -/** - * @brief Options to change the behaviour of the underlying websocket clients - **/ -struct PubSubClientOptions { - std::chrono::seconds pingInterval_; -}; - -} // namespace chatterino diff --git a/src/providers/twitch/PubSubHelpers.hpp b/src/providers/twitch/PubSubHelpers.hpp deleted file mode 100644 index 8f6ae124..00000000 --- a/src/providers/twitch/PubSubHelpers.hpp +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include "common/QLogging.hpp" - -#include -#include - -#include - -namespace chatterino { - -class TwitchAccount; -struct ActionUser; - -// Create timer using given ioService -template -void runAfter(boost::asio::io_context &ioc, Duration duration, Callback cb) -{ - auto timer = std::make_shared(ioc); - timer->expires_after(duration); - - timer->async_wait([timer, cb](const boost::system::error_code &ec) { - if (ec) - { - qCDebug(chatterinoPubSub) - << "Error in runAfter:" << ec.message().c_str(); - return; - } - - cb(timer); - }); -} - -// Use provided timer -template -void runAfter(std::shared_ptr timer, - Duration duration, Callback cb) -{ - timer->expires_after(duration); - - timer->async_wait([timer, cb](const boost::system::error_code &ec) { - if (ec) - { - qCDebug(chatterinoPubSub) - << "Error in runAfter:" << ec.message().c_str(); - return; - } - - cb(timer); - }); -} - -} // namespace chatterino diff --git a/src/providers/twitch/PubSubManager.cpp b/src/providers/twitch/PubSubManager.cpp index 5eb4d782..b82a1b23 100644 --- a/src/providers/twitch/PubSubManager.cpp +++ b/src/providers/twitch/PubSubManager.cpp @@ -2,148 +2,89 @@ #include "Application.hpp" #include "common/QLogging.hpp" -#include "providers/NetworkConfigurationProvider.hpp" +#include "providers/liveupdates/BasicPubSubManager.hpp" #include "providers/twitch/PubSubClient.hpp" -#include "providers/twitch/PubSubHelpers.hpp" -#include "providers/twitch/PubSubMessages.hpp" -#include "util/DebugCount.hpp" -#include "util/RenameThread.hpp" #include -#include -#include -#include #include -#include +#include -using websocketpp::lib::bind; -using websocketpp::lib::placeholders::_1; -using websocketpp::lib::placeholders::_2; using namespace std::chrono_literals; namespace chatterino { -PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) - : host_(host) - , clientOptions_({ - pingInterval, - }) +class PubSubManagerPrivate + : public BasicPubSubManager { - this->websocketClient.set_access_channels(websocketpp::log::alevel::all); - this->websocketClient.clear_access_channels( - websocketpp::log::alevel::frame_payload | - websocketpp::log::alevel::frame_header); +public: + PubSubManagerPrivate(PubSub &parent, QString host, + std::chrono::milliseconds heartbeatInterval); + ~PubSubManagerPrivate() override; + PubSubManagerPrivate(const PubSubManagerPrivate &) = delete; + PubSubManagerPrivate(PubSubManagerPrivate &&) = delete; + PubSubManagerPrivate &operator=(const PubSubManagerPrivate &) = delete; + PubSubManagerPrivate &operator=(PubSubManagerPrivate &&) = delete; - this->websocketClient.init_asio(); + std::shared_ptr makeClient(); + void checkHeartbeats(); - // SSL Handshake - this->websocketClient.set_tls_init_handler( - bind(&PubSub::onTLSInit, this, ::_1)); + std::chrono::milliseconds heartbeatInterval; + QTimer heartbeatTimer; - this->websocketClient.set_message_handler( - bind(&PubSub::onMessage, this, ::_1, ::_2)); - this->websocketClient.set_open_handler( - bind(&PubSub::onConnectionOpen, this, ::_1)); - this->websocketClient.set_close_handler( - bind(&PubSub::onConnectionClose, this, ::_1)); - this->websocketClient.set_fail_handler( - bind(&PubSub::onConnectionFail, this, ::_1)); + PubSub &parent; + + friend BasicPubSubManager; + friend PubSub; +}; + +PubSubManagerPrivate::PubSubManagerPrivate( + PubSub &parent, QString host, std::chrono::milliseconds heartbeatInterval) + : BasicPubSubManager(std::move(host), "PubSub") + , heartbeatInterval(heartbeatInterval) + , parent(parent) +{ + QObject::connect(&this->heartbeatTimer, &QTimer::timeout, this, + &PubSubManagerPrivate::checkHeartbeats); + this->heartbeatTimer.setInterval(this->heartbeatInterval); + this->heartbeatTimer.setSingleShot(false); + this->heartbeatTimer.start(); } -PubSub::~PubSub() +PubSubManagerPrivate::~PubSubManagerPrivate() { this->stop(); } -void PubSub::initialize() +void PubSubManagerPrivate::checkHeartbeats() { - this->start(); + for (const auto &[id, client] : this->clients()) + { + client->checkHeartbeat(); + } } -void PubSub::addClient() +std::shared_ptr PubSubManagerPrivate::makeClient() { - if (this->addingClient) - { - return; - } - - qCDebug(chatterinoPubSub) << "Adding an additional client"; - - this->addingClient = true; - - websocketpp::lib::error_code ec; - auto con = - this->websocketClient.get_connection(this->host_.toStdString(), ec); - - if (ec) - { - qCDebug(chatterinoPubSub) - << "Unable to establish connection:" << ec.message().c_str(); - return; - } - - NetworkConfigurationProvider::applyToWebSocket(con); - - this->websocketClient.connect(con); + return std::make_shared(this->parent, + this->heartbeatInterval); } -void PubSub::start() +PubSub::PubSub(const QString &host, std::chrono::seconds heartbeatInterval) + : private_(new PubSubManagerPrivate(*this, host, heartbeatInterval)) { - this->work = std::make_shared>( - this->websocketClient.get_io_service().get_executor()); - this->thread = std::make_unique([this] { - // make sure we set in any case, even exceptions - auto guard = qScopeGuard([&] { - this->stoppedFlag_.set(); - }); +} - runThread(); - }); - renameThread(*this->thread, "PubSub"); +PubSub::~PubSub() = default; + +const liveupdates::Diag &PubSub::wsDiag() const +{ + return this->private_->diag; } void PubSub::stop() { - this->stopping_ = true; - - for (const auto &[hdl, client] : this->clients) - { - (void)hdl; - - client->close("Shutting down"); - } - - this->work.reset(); - - if (!this->thread->joinable()) - { - return; - } - - // NOTE: - // There is a case where a new client was initiated but not added to the clients list. - // We just don't join the thread & let the operating system nuke the thread if joining fails - // within 1s. - // We could fix the underlying bug, but this is easier & we realistically won't use this exact code - // for super much longer. - if (this->stoppedFlag_.waitFor(std::chrono::milliseconds{100})) - { - this->thread->join(); - return; - } - - qCWarning(chatterinoLiveupdates) - << "Thread didn't finish within 100ms, force-stop the client"; - this->websocketClient.stop(); - if (this->stoppedFlag_.waitFor(std::chrono::milliseconds{20})) - { - this->thread->join(); - return; - } - - qCWarning(chatterinoLiveupdates) << "Thread didn't finish after stopping"; + this->private_->stop(); } void PubSub::listenToChannelPointRewards(const QString &channelID) @@ -153,426 +94,8 @@ void PubSub::listenToChannelPointRewards(const QString &channelID) auto topic = topicFormat.arg(channelID); - if (this->isListeningToTopic(topic)) - { - return; - } qCDebug(chatterinoPubSub) << "Listen to topic" << topic; - - this->listenToTopic(topic); -} - -void PubSub::unlistenChannelPointRewards() -{ - this->unlistenPrefix("community-points-channel-v1."); -} - -void PubSub::unlistenPrefix(const QString &prefix) -{ - for (const auto &p : this->clients) - { - const auto &client = p.second; - if (const auto &[topics, nonce] = client->unlistenPrefix(prefix); - !topics.empty()) - { - NonceInfo nonceInfo{ - client, - "UNLISTEN", - topics, - topics.size(), - }; - this->registerNonce(nonce, nonceInfo); - } - } -} - -void PubSub::listen(PubSubListenMessage msg) -{ - if (this->tryListen(msg)) - { - return; - } - - this->addClient(); - - std::copy(msg.topics.begin(), msg.topics.end(), - std::back_inserter(this->requests)); - - DebugCount::increase("PubSub topic backlog", msg.topics.size()); -} - -bool PubSub::tryListen(PubSubListenMessage msg) -{ - for (const auto &p : this->clients) - { - const auto &client = p.second; - if (auto success = client->listen(msg); success) - { - this->registerNonce(msg.nonce, { - client, - "LISTEN", - msg.topics, - msg.topics.size(), - }); - return true; - } - } - - return false; -} - -void PubSub::registerNonce(QString nonce, NonceInfo info) -{ - this->nonces_[nonce] = std::move(info); -} - -std::optional PubSub::findNonceInfo(QString nonce) -{ - // TODO: This should also DELETE the nonceinfo from the map - auto it = this->nonces_.find(nonce); - - if (it == this->nonces_.end()) - { - return std::nullopt; - } - - return it->second; -} - -bool PubSub::isListeningToTopic(const QString &topic) -{ - for (const auto &p : this->clients) - { - const auto &client = p.second; - if (client->isListeningToTopic(topic)) - { - return true; - } - } - - return false; -} - -void PubSub::onMessage(websocketpp::connection_hdl hdl, - WebsocketMessagePtr websocketMessage) -{ - this->diag.messagesReceived += 1; - - const auto &payload = - QString::fromStdString(websocketMessage->get_payload()); - - auto oMessage = parsePubSubBaseMessage(payload); - - if (!oMessage) - { - qCDebug(chatterinoPubSub) - << "Unable to parse incoming pubsub message" << payload; - this->diag.messagesFailedToParse += 1; - return; - } - - auto message = *oMessage; - - switch (message.type) - { - case PubSubMessage::Type::Pong: { - auto clientIt = this->clients.find(hdl); - - // If this assert goes off, there's something wrong with the connection - // creation/preserving code KKona - assert(clientIt != this->clients.end()); - - auto &client = *clientIt; - - client.second->handlePong(); - } - break; - - case PubSubMessage::Type::Response: { - this->handleResponse(message); - } - break; - - case PubSubMessage::Type::Message: { - auto oMessageMessage = message.toInner(); - if (!oMessageMessage) - { - qCDebug(chatterinoPubSub) << "Malformed MESSAGE:" << payload; - return; - } - - this->handleMessageResponse(*oMessageMessage); - } - break; - - case PubSubMessage::Type::INVALID: - default: { - qCDebug(chatterinoPubSub) - << "Unknown message type:" << message.typeString; - } - break; - } -} - -void PubSub::onConnectionOpen(WebsocketHandle hdl) -{ - this->diag.connectionsOpened += 1; - - DebugCount::increase("PubSub connections"); - this->addingClient = false; - - this->connectBackoff.reset(); - - auto client = std::make_shared(this->websocketClient, hdl, - this->clientOptions_); - - // We separate the starting from the constructor because we will want to use - // shared_from_this - client->start(); - - this->clients.emplace(hdl, client); - - qCDebug(chatterinoPubSub) << "PubSub connection opened!"; - - const auto topicsToTake = - std::min(this->requests.size(), PubSubClient::MAX_LISTENS); - - std::vector newTopics( - std::make_move_iterator(this->requests.begin()), - std::make_move_iterator(this->requests.begin() + topicsToTake)); - - this->requests.erase(this->requests.begin(), - this->requests.begin() + topicsToTake); - - PubSubListenMessage msg(newTopics); - - if (auto success = client->listen(msg); !success) - { - qCWarning(chatterinoPubSub) << "Failed to listen to " << topicsToTake - << "new topics on new client"; - return; - } - DebugCount::decrease("PubSub topic backlog", msg.topics.size()); - - this->registerNonce(msg.nonce, { - client, - "LISTEN", - msg.topics, - topicsToTake, - }); - - if (!this->requests.empty()) - { - this->addClient(); - } -} - -void PubSub::onConnectionFail(WebsocketHandle hdl) -{ - this->diag.connectionsFailed += 1; - - DebugCount::increase("PubSub failed connections"); - if (auto conn = this->websocketClient.get_con_from_hdl(std::move(hdl))) - { - qCDebug(chatterinoPubSub) << "PubSub connection attempt failed (error: " - << conn->get_ec().message().c_str() << ")"; - } - else - { - qCDebug(chatterinoPubSub) - << "PubSub connection attempt failed but we can't " - "get the connection from a handle."; - } - - this->addingClient = false; - if (!this->requests.empty()) - { - runAfter(this->websocketClient.get_io_service(), - this->connectBackoff.next(), [this](auto timer) { - this->addClient(); // - }); - } -} - -void PubSub::onConnectionClose(WebsocketHandle hdl) -{ - qCDebug(chatterinoPubSub) << "Connection closed"; - this->diag.connectionsClosed += 1; - - DebugCount::decrease("PubSub connections"); - auto clientIt = this->clients.find(hdl); - - // If this assert goes off, there's something wrong with the connection - // creation/preserving code KKona - assert(clientIt != this->clients.end()); - - auto client = clientIt->second; - - this->clients.erase(clientIt); - - client->stop(); - - if (!this->stopping_) - { - auto clientListeners = client->getListeners(); - for (const auto &listener : clientListeners) - { - this->listenToTopic(listener.topic); - } - } -} - -PubSub::WebsocketContextPtr PubSub::onTLSInit(websocketpp::connection_hdl hdl) -{ - WebsocketContextPtr ctx( - new boost::asio::ssl::context(boost::asio::ssl::context::tlsv12)); - - try - { - ctx->set_options(boost::asio::ssl::context::default_workarounds | - boost::asio::ssl::context::no_sslv2 | - boost::asio::ssl::context::single_dh_use); - } - catch (const std::exception &e) - { - qCDebug(chatterinoPubSub) - << "Exception caught in OnTLSInit:" << e.what(); - } - - return ctx; -} - -void PubSub::handleResponse(const PubSubMessage &message) -{ - const bool failed = !message.error.isEmpty(); - - if (failed) - { - qCDebug(chatterinoPubSub) - << "Error" << message.error << "on nonce" << message.nonce; - } - - if (message.nonce.isEmpty()) - { - // Can't do any specific handling since no nonce was specified - return; - } - - if (auto oInfo = this->findNonceInfo(message.nonce); oInfo) - { - const auto info = *oInfo; - auto client = info.client.lock(); - if (!client) - { - qCDebug(chatterinoPubSub) << "Client associated with nonce" - << message.nonce << "is no longer alive"; - return; - } - if (info.messageType == "LISTEN") - { - client->handleListenResponse(message); - this->handleListenResponse(info, failed); - } - else if (info.messageType == "UNLISTEN") - { - client->handleUnlistenResponse(message); - this->handleUnlistenResponse(info, failed); - } - else - { - qCDebug(chatterinoPubSub) - << "Unhandled nonce message type" << info.messageType; - } - - return; - } - - qCDebug(chatterinoPubSub) << "Response on unused" << message.nonce - << "client/topic listener mismatch?"; -} - -void PubSub::handleListenResponse(const NonceInfo &info, bool failed) -{ - DebugCount::decrease("PubSub topic pending listens", info.topicCount); - if (failed) - { - this->diag.failedListenResponses++; - DebugCount::increase("PubSub topic failed listens", info.topicCount); - } - else - { - this->diag.listenResponses++; - DebugCount::increase("PubSub topic listening", info.topicCount); - } -} - -void PubSub::handleUnlistenResponse(const NonceInfo &info, bool failed) -{ - this->diag.unlistenResponses++; - DebugCount::decrease("PubSub topic pending unlistens", info.topicCount); - if (failed) - { - qCDebug(chatterinoPubSub) << "Failed unlistening to" << info.topics; - DebugCount::increase("PubSub topic failed unlistens", info.topicCount); - } - else - { - qCDebug(chatterinoPubSub) << "Successful unlistened to" << info.topics; - DebugCount::decrease("PubSub topic listening", info.topicCount); - } -} - -void PubSub::handleMessageResponse(const PubSubMessageMessage &message) -{ - QString topic = message.topic; - - if (topic.startsWith("community-points-channel-v1.")) - { - auto oInnerMessage = - message.toInner(); - if (!oInnerMessage) - { - return; - } - - auto innerMessage = *oInnerMessage; - - switch (innerMessage.type) - { - case PubSubCommunityPointsChannelV1Message::Type:: - AutomaticRewardRedeemed: - case PubSubCommunityPointsChannelV1Message::Type::RewardRedeemed: { - auto redemption = - innerMessage.data.value("redemption").toObject(); - this->pointReward.redeemed.invoke(redemption); - } - break; - - case PubSubCommunityPointsChannelV1Message::Type::INVALID: - default: { - qCDebug(chatterinoPubSub) - << "Invalid point event type:" << innerMessage.typeString; - } - break; - } - } - else - { - qCDebug(chatterinoPubSub) << "Unknown topic:" << topic; - return; - } -} - -void PubSub::runThread() -{ - qCDebug(chatterinoPubSub) << "Start pubsub manager thread"; - this->websocketClient.run(); - qCDebug(chatterinoPubSub) << "Done with pubsub manager thread"; -} - -void PubSub::listenToTopic(const QString &topic) -{ - this->listen(PubSubListenMessage({topic})); + this->private_->subscribe(TopicData{.topic = std::move(topic)}); } } // namespace chatterino diff --git a/src/providers/twitch/PubSubManager.hpp b/src/providers/twitch/PubSubManager.hpp index 6fd086a6..421ce47a 100644 --- a/src/providers/twitch/PubSubManager.hpp +++ b/src/providers/twitch/PubSubManager.hpp @@ -1,29 +1,15 @@ #pragma once -#include "providers/twitch/PubSubClientOptions.hpp" -#include "providers/twitch/PubSubWebsocket.hpp" -#include "util/ExponentialBackoff.hpp" -#include "util/OnceFlag.hpp" +#include "providers/liveupdates/Diag.hpp" -#include -#include #include #include #include -#include -#include -#include -#include #include #include #include -#include #include -#include -#include -#include -#include #if __has_include() # include @@ -31,12 +17,7 @@ namespace chatterino { -class TwitchAccount; -class PubSubClient; - -struct PubSubListenMessage; -struct PubSubMessage; -struct PubSubMessageMessage; +class PubSubManagerPrivate; /** * This handles the Twitch PubSub connection @@ -47,28 +28,12 @@ struct PubSubMessageMessage; */ class PubSub { - using WebsocketMessagePtr = - websocketpp::config::asio_tls_client::message_type::ptr; - using WebsocketContextPtr = - websocketpp::lib::shared_ptr; - template - using Signal = - pajlada::Signals::Signal; // type-id is vector> - - struct NonceInfo { - std::weak_ptr client; - QString messageType; // e.g. LISTEN or UNLISTEN - std::vector topics; - std::vector::size_type topicCount; - }; - - WebsocketClient websocketClient; - std::unique_ptr thread; + using Signal = pajlada::Signals::Signal; public: PubSub(const QString &host, - std::chrono::seconds pingInterval = std::chrono::seconds(15)); + std::chrono::seconds heartbeatInterval = std::chrono::seconds(15)); ~PubSub(); PubSub(const PubSub &) = delete; @@ -76,9 +41,6 @@ public: PubSub &operator=(const PubSub &) = delete; PubSub &operator=(PubSub &&) = delete; - /// Set up connections between itself & other parts of the application - void initialize(); - struct { Signal redeemed; } pointReward; @@ -90,12 +52,8 @@ public: * PubSub topic: community-points-channel-v1.{channelID} */ void listenToChannelPointRewards(const QString &channelID); - void unlistenChannelPointRewards(); struct { - std::atomic connectionsClosed{0}; - std::atomic connectionsOpened{0}; - std::atomic connectionsFailed{0}; std::atomic messagesReceived{0}; std::atomic messagesFailedToParse{0}; std::atomic failedListenResponses{0}; @@ -103,64 +61,15 @@ public: std::atomic unlistenResponses{0}; } diag; + /// Statistics about the opened/closed connections and received messages + /// + /// Used in tests. + const liveupdates::Diag &wsDiag() const; + private: - void start(); void stop(); - /** - * Unlistens to all topics matching the prefix in all clients - */ - void unlistenPrefix(const QString &prefix); - - void listenToTopic(const QString &topic); - - void listen(PubSubListenMessage msg); - bool tryListen(PubSubListenMessage msg); - - bool isListeningToTopic(const QString &topic); - - void addClient(); - - std::vector requests; - - std::atomic addingClient{false}; - ExponentialBackoff<5> connectBackoff{std::chrono::milliseconds(1000)}; - - std::map, - std::owner_less> - clients; - - void onMessage(websocketpp::connection_hdl hdl, WebsocketMessagePtr msg); - void onConnectionOpen(websocketpp::connection_hdl hdl); - void onConnectionFail(websocketpp::connection_hdl hdl); - void onConnectionClose(websocketpp::connection_hdl hdl); - WebsocketContextPtr onTLSInit(websocketpp::connection_hdl hdl); - - void handleResponse(const PubSubMessage &message); - void handleListenResponse(const NonceInfo &info, bool failed); - void handleUnlistenResponse(const NonceInfo &info, bool failed); - void handleMessageResponse(const PubSubMessageMessage &message); - - // Register a nonce for a specific client - void registerNonce(QString nonce, NonceInfo nonceInfo); - - // Find client associated with a nonce - std::optional findNonceInfo(QString nonce); - - std::unordered_map nonces_; - - void runThread(); - - std::shared_ptr> - work{nullptr}; - - const QString host_; - const PubSubClientOptions clientOptions_; - - OnceFlag stoppedFlag_; - - bool stopping_{false}; + std::unique_ptr private_; #ifdef FRIEND_TEST friend class FTest; diff --git a/src/providers/twitch/PubSubWebsocket.hpp b/src/providers/twitch/PubSubWebsocket.hpp deleted file mode 100644 index 068b3793..00000000 --- a/src/providers/twitch/PubSubWebsocket.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include "providers/twitch/ChatterinoWebSocketppLogger.hpp" - -#include -#include -#include -#include - -namespace chatterino { - -struct chatterinoconfig : public websocketpp::config::asio_tls_client { - typedef websocketpp::log::chatterinowebsocketpplogger< - concurrency_type, websocketpp::log::elevel> - elog_type; - typedef websocketpp::log::chatterinowebsocketpplogger< - concurrency_type, websocketpp::log::alevel> - alog_type; - - struct permessage_deflate_config { - }; - - typedef websocketpp::extensions::permessage_deflate::disabled< - permessage_deflate_config> - permessage_deflate_type; -}; - -using WebsocketClient = websocketpp::client; -using WebsocketHandle = websocketpp::connection_hdl; -using WebsocketErrorCode = websocketpp::lib::error_code; - -} // namespace chatterino diff --git a/src/providers/twitch/pubsubmessages/Base.cpp b/src/providers/twitch/pubsubmessages/Base.cpp index 4b32786e..63576dc9 100644 --- a/src/providers/twitch/pubsubmessages/Base.cpp +++ b/src/providers/twitch/pubsubmessages/Base.cpp @@ -18,9 +18,9 @@ PubSubMessage::PubSubMessage(QJsonObject _object) } } -std::optional parsePubSubBaseMessage(const QString &blob) +std::optional parsePubSubBaseMessage(const QByteArray &blob) { - QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8())); + QJsonDocument jsonDoc(QJsonDocument::fromJson(blob)); if (jsonDoc.isNull()) { diff --git a/src/providers/twitch/pubsubmessages/Base.hpp b/src/providers/twitch/pubsubmessages/Base.hpp index a1da168c..f8ccfc29 100644 --- a/src/providers/twitch/pubsubmessages/Base.hpp +++ b/src/providers/twitch/pubsubmessages/Base.hpp @@ -45,7 +45,7 @@ std::optional PubSubMessage::toInner() return InnerClass{this->nonce, data}; } -std::optional parsePubSubBaseMessage(const QString &blob); +std::optional parsePubSubBaseMessage(const QByteArray &blob); } // namespace chatterino diff --git a/src/util/SampleData.cpp b/src/util/SampleData.cpp index 48429cbf..6dd756a3 100644 --- a/src/util/SampleData.cpp +++ b/src/util/SampleData.cpp @@ -2,6 +2,8 @@ namespace chatterino { +using namespace Qt::Literals; + /// Sample messages coming from IRC const QStringList &getSampleCheerMessages() @@ -163,20 +165,14 @@ const QStringList &getSampleEmoteTestMessages() /// Channel point reward tests -const QString &getSampleChannelRewardMessage() +QByteArray getSampleChannelRewardMessage() { - static QString str{ - R"({"type":"MESSAGE","data":{"topic":"community-points-channel-v1.11148817","message":"{\"type\":\"reward-redeemed\",\"data\":{\"timestamp\":\"2022-11-19T13:36:49.536938653Z\",\"redemption\":{\"id\":\"fd8af65d-3532-4e91-b30e-3995cefe576b\",\"user\":{\"id\":\"11148817\",\"login\":\"pajlada\",\"display_name\":\"pajlada\"},\"channel_id\":\"11148817\",\"redeemed_at\":\"2022-11-19T13:36:49.536938653Z\",\"reward\":{\"id\":\"44c3554e-58bc-4753-bf94-b615931d1072\",\"channel_id\":\"11148817\",\"title\":\"Test\",\"prompt\":\"\",\"cost\":1,\"is_user_input_required\":false,\"is_sub_only\":false,\"image\":{\"url_1x\":\"https://static-cdn.jtvnw.net/custom-reward-images/11148817/44c3554e-58bc-4753-bf94-b615931d1072/effc017c-861d-410d-abff-8bd98f1937eb/custom-1.png\",\"url_2x\":\"https://static-cdn.jtvnw.net/custom-reward-images/11148817/44c3554e-58bc-4753-bf94-b615931d1072/effc017c-861d-410d-abff-8bd98f1937eb/custom-2.png\",\"url_4x\":\"https://static-cdn.jtvnw.net/custom-reward-images/11148817/44c3554e-58bc-4753-bf94-b615931d1072/effc017c-861d-410d-abff-8bd98f1937eb/custom-4.png\"},\"default_image\":{\"url_1x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-1.png\",\"url_2x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-2.png\",\"url_4x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-4.png\"},\"background_color\":\"#9147FF\",\"is_enabled\":true,\"is_paused\":false,\"is_in_stock\":true,\"max_per_stream\":{\"is_enabled\":false,\"max_per_stream\":0},\"should_redemptions_skip_request_queue\":false,\"template_id\":null,\"updated_for_indicator_at\":\"2021-03-16T00:40:29.385327044Z\",\"max_per_user_per_stream\":{\"is_enabled\":false,\"max_per_user_per_stream\":0},\"global_cooldown\":{\"is_enabled\":false,\"global_cooldown_seconds\":0},\"redemptions_redeemed_current_stream\":null,\"cooldown_expires_at\":null},\"status\":\"UNFULFILLED\",\"cursor\":\"ZmQ4YWY2NWQtMzUzMi00ZTkxLWIzMGUtMzk5NWNlZmU1NzZiX18yMDIyLTExLTE5VDEzOjM2OjQ5LjUzNjkzODY1M1o=\"}}}"}})", - }; - return str; + return R"({"type":"MESSAGE","data":{"topic":"community-points-channel-v1.11148817","message":"{\"type\":\"reward-redeemed\",\"data\":{\"timestamp\":\"2022-11-19T13:36:49.536938653Z\",\"redemption\":{\"id\":\"fd8af65d-3532-4e91-b30e-3995cefe576b\",\"user\":{\"id\":\"11148817\",\"login\":\"pajlada\",\"display_name\":\"pajlada\"},\"channel_id\":\"11148817\",\"redeemed_at\":\"2022-11-19T13:36:49.536938653Z\",\"reward\":{\"id\":\"44c3554e-58bc-4753-bf94-b615931d1072\",\"channel_id\":\"11148817\",\"title\":\"Test\",\"prompt\":\"\",\"cost\":1,\"is_user_input_required\":false,\"is_sub_only\":false,\"image\":{\"url_1x\":\"https://static-cdn.jtvnw.net/custom-reward-images/11148817/44c3554e-58bc-4753-bf94-b615931d1072/effc017c-861d-410d-abff-8bd98f1937eb/custom-1.png\",\"url_2x\":\"https://static-cdn.jtvnw.net/custom-reward-images/11148817/44c3554e-58bc-4753-bf94-b615931d1072/effc017c-861d-410d-abff-8bd98f1937eb/custom-2.png\",\"url_4x\":\"https://static-cdn.jtvnw.net/custom-reward-images/11148817/44c3554e-58bc-4753-bf94-b615931d1072/effc017c-861d-410d-abff-8bd98f1937eb/custom-4.png\"},\"default_image\":{\"url_1x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-1.png\",\"url_2x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-2.png\",\"url_4x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-4.png\"},\"background_color\":\"#9147FF\",\"is_enabled\":true,\"is_paused\":false,\"is_in_stock\":true,\"max_per_stream\":{\"is_enabled\":false,\"max_per_stream\":0},\"should_redemptions_skip_request_queue\":false,\"template_id\":null,\"updated_for_indicator_at\":\"2021-03-16T00:40:29.385327044Z\",\"max_per_user_per_stream\":{\"is_enabled\":false,\"max_per_user_per_stream\":0},\"global_cooldown\":{\"is_enabled\":false,\"global_cooldown_seconds\":0},\"redemptions_redeemed_current_stream\":null,\"cooldown_expires_at\":null},\"status\":\"UNFULFILLED\",\"cursor\":\"ZmQ4YWY2NWQtMzUzMi00ZTkxLWIzMGUtMzk5NWNlZmU1NzZiX18yMDIyLTExLTE5VDEzOjM2OjQ5LjUzNjkzODY1M1o=\"}}}"}})"_ba; } -const QString &getSampleChannelRewardMessage2() +QByteArray getSampleChannelRewardMessage2() { - static QString str{ - R"({"type":"MESSAGE","data":{"topic":"community-points-channel-v1.11148817","message":"{\"type\":\"reward-redeemed\",\"data\":{\"timestamp\":\"2022-11-19T13:37:55.860377616Z\",\"redemption\":{\"id\":\"e8712d29-7564-40e9-a972-d70997665605\",\"user\":{\"id\":\"11148817\",\"login\":\"pajlada\",\"display_name\":\"pajlada\"},\"channel_id\":\"11148817\",\"redeemed_at\":\"2022-11-19T13:37:55.860377616Z\",\"reward\":{\"id\":\"649ecb1f-3fa8-491e-a48d-bd50720929d9\",\"channel_id\":\"11148817\",\"title\":\"asdd\",\"prompt\":\"asd\",\"cost\":5,\"is_user_input_required\":true,\"is_sub_only\":false,\"image\":null,\"default_image\":{\"url_1x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-1.png\",\"url_2x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-2.png\",\"url_4x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-4.png\"},\"background_color\":\"#0013A3\",\"is_enabled\":true,\"is_paused\":false,\"is_in_stock\":true,\"max_per_stream\":{\"is_enabled\":false,\"max_per_stream\":0},\"should_redemptions_skip_request_queue\":false,\"template_id\":null,\"updated_for_indicator_at\":\"2021-03-16T00:45:25.236177469Z\",\"max_per_user_per_stream\":{\"is_enabled\":false,\"max_per_user_per_stream\":0},\"global_cooldown\":{\"is_enabled\":false,\"global_cooldown_seconds\":0},\"redemptions_redeemed_current_stream\":null,\"cooldown_expires_at\":null},\"user_input\":\"TEST\",\"status\":\"UNFULFILLED\",\"cursor\":\"ZTg3MTJkMjktNzU2NC00MGU5LWE5NzItZDcwOTk3NjY1NjA1X18yMDIyLTExLTE5VDEzOjM3OjU1Ljg2MDM3NzYxNlo=\"}}}"}})", - }; - return str; + return R"({"type":"MESSAGE","data":{"topic":"community-points-channel-v1.11148817","message":"{\"type\":\"reward-redeemed\",\"data\":{\"timestamp\":\"2022-11-19T13:37:55.860377616Z\",\"redemption\":{\"id\":\"e8712d29-7564-40e9-a972-d70997665605\",\"user\":{\"id\":\"11148817\",\"login\":\"pajlada\",\"display_name\":\"pajlada\"},\"channel_id\":\"11148817\",\"redeemed_at\":\"2022-11-19T13:37:55.860377616Z\",\"reward\":{\"id\":\"649ecb1f-3fa8-491e-a48d-bd50720929d9\",\"channel_id\":\"11148817\",\"title\":\"asdd\",\"prompt\":\"asd\",\"cost\":5,\"is_user_input_required\":true,\"is_sub_only\":false,\"image\":null,\"default_image\":{\"url_1x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-1.png\",\"url_2x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-2.png\",\"url_4x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-4.png\"},\"background_color\":\"#0013A3\",\"is_enabled\":true,\"is_paused\":false,\"is_in_stock\":true,\"max_per_stream\":{\"is_enabled\":false,\"max_per_stream\":0},\"should_redemptions_skip_request_queue\":false,\"template_id\":null,\"updated_for_indicator_at\":\"2021-03-16T00:45:25.236177469Z\",\"max_per_user_per_stream\":{\"is_enabled\":false,\"max_per_user_per_stream\":0},\"global_cooldown\":{\"is_enabled\":false,\"global_cooldown_seconds\":0},\"redemptions_redeemed_current_stream\":null,\"cooldown_expires_at\":null},\"user_input\":\"TEST\",\"status\":\"UNFULFILLED\",\"cursor\":\"ZTg3MTJkMjktNzU2NC00MGU5LWE5NzItZDcwOTk3NjY1NjA1X18yMDIyLTExLTE5VDEzOjM3OjU1Ljg2MDM3NzYxNlo=\"}}}"}})"_ba; } const QString &getSampleChannelRewardIRCMessage() diff --git a/src/util/SampleData.hpp b/src/util/SampleData.hpp index 53d22f1e..b4a2c3c6 100644 --- a/src/util/SampleData.hpp +++ b/src/util/SampleData.hpp @@ -13,8 +13,8 @@ const QStringList &getSampleEmoteTestMessages(); /// Channel point reward tests -const QString &getSampleChannelRewardMessage(); -const QString &getSampleChannelRewardMessage2(); +QByteArray getSampleChannelRewardMessage(); +QByteArray getSampleChannelRewardMessage2(); const QString &getSampleChannelRewardIRCMessage(); /// Links diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 1d652961..b72a377b 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -98,9 +98,6 @@ AboutPage::AboutPage() addLicense(form.getElement(), "Pajlada/Serialize", "https://github.com/pajlada/serialize", ":/licenses/pajlada_serialize.txt"); - addLicense(form.getElement(), "Websocketpp", - "https://www.zaphoyd.com/websocketpp/", - ":/licenses/websocketpp.txt"); #ifndef NO_QTKEYCHAIN addLicense(form.getElement(), "QtKeychain", "https://github.com/frankosterfeld/qtkeychain", diff --git a/tests/src/BasicPubSub.cpp b/tests/src/BasicPubSub.cpp index e6de95f7..88e67adb 100644 --- a/tests/src/BasicPubSub.cpp +++ b/tests/src/BasicPubSub.cpp @@ -73,7 +73,7 @@ struct hash { namespace { class MyManager; -class MyClient : public BasicPubSubClient +class MyClient : public BasicPubSubClient { public: MyClient(MyManager &manager) diff --git a/tests/src/TwitchPubSubClient.cpp b/tests/src/TwitchPubSubClient.cpp index 9bff8b00..e4865e62 100644 --- a/tests/src/TwitchPubSubClient.cpp +++ b/tests/src/TwitchPubSubClient.cpp @@ -1,10 +1,10 @@ #include "mocks/BaseApplication.hpp" #include "providers/twitch/PubSubClient.hpp" #include "providers/twitch/PubSubManager.hpp" -#include "providers/twitch/TwitchAccount.hpp" #include "Test.hpp" #include +#include #include #include @@ -107,38 +107,40 @@ TEST(TwitchPubSubClient, ServerRespondsToPings) MockApplication a("", 1s); auto &pubSub = a.pubSub; - pubSub.start(); + QTest::qWait(50); - std::this_thread::sleep_for(50ms); - - ASSERT_EQ(pubSub.diag.connectionsOpened, 0); - ASSERT_EQ(pubSub.diag.connectionsClosed, 0); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); ASSERT_EQ(pubSub.diag.messagesReceived, 0); pubSub.listenToChannelPointRewards("123456"); - std::this_thread::sleep_for(150ms); + QTest::qWait(150); - ASSERT_EQ(pubSub.diag.connectionsOpened, 1); - ASSERT_EQ(pubSub.diag.connectionsClosed, 0); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); - ASSERT_EQ(pubSub.diag.messagesReceived, 2); + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 1); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); + ASSERT_EQ(pubSub.diag.messagesReceived, 1); // LISTEN ASSERT_EQ(pubSub.diag.listenResponses, 1); - std::this_thread::sleep_for(2s); + QTest::qWait(2 * 1000); - ASSERT_EQ(pubSub.diag.connectionsOpened, 1); - ASSERT_EQ(pubSub.diag.connectionsClosed, 0); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); - ASSERT_EQ(pubSub.diag.messagesReceived, 4); + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 1); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); + ASSERT_EQ(pubSub.diag.messagesReceived, 3); // LISTEN + 2 * PONG pubSub.stop(); - ASSERT_EQ(pubSub.diag.connectionsOpened, 1); - ASSERT_EQ(pubSub.diag.connectionsClosed, 1); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); - ASSERT_EQ(pubSub.diag.messagesReceived, 4); + // after exactly one event loop iteration, we should see updated counters + QCoreApplication::processEvents(QEventLoop::AllEvents); + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 1); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 1); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); + ASSERT_EQ(pubSub.diag.messagesReceived, 3); ASSERT_EQ(pubSub.diag.listenResponses, 1); } @@ -147,28 +149,31 @@ TEST(TwitchPubSubClient, ServerDoesntRespondToPings) MockApplication a("/dont-respond-to-ping", 1s); auto &pubSub = a.pubSub; - pubSub.start(); pubSub.listenToChannelPointRewards("123456"); - std::this_thread::sleep_for(750ms); + QTest::qWait(750); - ASSERT_EQ(pubSub.diag.connectionsOpened, 1); - ASSERT_EQ(pubSub.diag.connectionsClosed, 0); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 1); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); ASSERT_EQ(pubSub.diag.messagesReceived, 1); - std::this_thread::sleep_for(500ms); + QTest::qWait(1500); // we need to wait for two rounds of pings - ASSERT_EQ(pubSub.diag.connectionsOpened, 2); - ASSERT_EQ(pubSub.diag.connectionsClosed, 1); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 2); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 1); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); ASSERT_EQ(pubSub.diag.messagesReceived, 2); pubSub.stop(); - ASSERT_EQ(pubSub.diag.connectionsOpened, 2); - ASSERT_EQ(pubSub.diag.connectionsClosed, 2); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); + // after exactly one event loop iteration, we should see updated counters + QCoreApplication::processEvents(QEventLoop::AllEvents); + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 2); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 2); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); ASSERT_EQ(pubSub.diag.messagesReceived, 2); } @@ -177,33 +182,31 @@ TEST(TwitchPubSubClient, DisconnectedAfter1s) MockApplication a("/disconnect-client-after-1s", 10s); auto &pubSub = a.pubSub; - pubSub.start(); + QTest::qWait(50); - std::this_thread::sleep_for(50ms); - - ASSERT_EQ(pubSub.diag.connectionsOpened, 0); - ASSERT_EQ(pubSub.diag.connectionsClosed, 0); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); ASSERT_EQ(pubSub.diag.messagesReceived, 0); ASSERT_EQ(pubSub.diag.listenResponses, 0); pubSub.listenToChannelPointRewards("123456"); - std::this_thread::sleep_for(500ms); + QTest::qWait(500); - ASSERT_EQ(pubSub.diag.connectionsOpened, 1); - ASSERT_EQ(pubSub.diag.connectionsClosed, 0); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); - ASSERT_EQ(pubSub.diag.messagesReceived, 2); // Listen RESPONSE & Pong + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 1); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); + ASSERT_EQ(pubSub.diag.messagesReceived, 1); // Listen RESPONSE ASSERT_EQ(pubSub.diag.listenResponses, 1); - std::this_thread::sleep_for(950ms); + QTest::qWait(950); - ASSERT_EQ(pubSub.diag.connectionsOpened, 2); - ASSERT_EQ(pubSub.diag.connectionsClosed, 1); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 2); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 1); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); ASSERT_EQ(pubSub.diag.listenResponses, 2); - ASSERT_EQ(pubSub.diag.messagesReceived, 4); // new listen & new pong + ASSERT_EQ(pubSub.diag.messagesReceived, 2); // new listen pubSub.stop(); } @@ -213,11 +216,9 @@ TEST(TwitchPubSubClient, ExceedTopicLimit) MockApplication a("", 1s); auto &pubSub = a.pubSub; - pubSub.start(); - - ASSERT_EQ(pubSub.diag.connectionsOpened, 0); - ASSERT_EQ(pubSub.diag.connectionsClosed, 0); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); ASSERT_EQ(pubSub.diag.messagesReceived, 0); for (size_t i = 0; i < PubSubClient::MAX_LISTENS; ++i) @@ -225,58 +226,32 @@ TEST(TwitchPubSubClient, ExceedTopicLimit) pubSub.listenToChannelPointRewards(QString("1%1").arg(i)); } - std::this_thread::sleep_for(50ms); + QTest::qWait(100); - ASSERT_EQ(pubSub.diag.connectionsOpened, 1); - ASSERT_EQ(pubSub.diag.connectionsClosed, 0); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 1); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); for (size_t i = 0; i < PubSubClient::MAX_LISTENS; ++i) { pubSub.listenToChannelPointRewards(QString("2%1").arg(i)); } - std::this_thread::sleep_for(50ms); + QTest::qWait(200); - ASSERT_EQ(pubSub.diag.connectionsOpened, 2); - ASSERT_EQ(pubSub.diag.connectionsClosed, 0); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 2); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); pubSub.stop(); - ASSERT_EQ(pubSub.diag.connectionsOpened, 2); - ASSERT_EQ(pubSub.diag.connectionsClosed, 2); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); -} + // after exactly one event loop iteration, we should see updated counters + QCoreApplication::processEvents(QEventLoop::AllEvents); + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); -TEST(TwitchPubSubClient, ExceedTopicLimitSingleStep) -{ - MockApplication a("", 1s); - auto &pubSub = a.pubSub; - - pubSub.start(); - - ASSERT_EQ(pubSub.diag.connectionsOpened, 0); - ASSERT_EQ(pubSub.diag.connectionsClosed, 0); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); - ASSERT_EQ(pubSub.diag.messagesReceived, 0); - - for (size_t i = 0; i < PubSubClient::MAX_LISTENS * 2; ++i) - { - pubSub.listenToChannelPointRewards("123456"); - } - - std::this_thread::sleep_for(150ms); - - ASSERT_EQ(pubSub.diag.connectionsOpened, 2); - ASSERT_EQ(pubSub.diag.connectionsClosed, 0); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); - - pubSub.stop(); - - ASSERT_EQ(pubSub.diag.connectionsOpened, 2); - ASSERT_EQ(pubSub.diag.connectionsClosed, 2); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); + ASSERT_EQ(pubSub.wsDiag().connectionsOpened, 2); + ASSERT_EQ(pubSub.wsDiag().connectionsClosed, 2); + ASSERT_EQ(pubSub.wsDiag().connectionsFailed, 0); } } // namespace chatterino