refactor: move Twitch PubSub to use liveupdates (#6638)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix
2025-12-16 11:06:15 +01:00
committed by GitHub
parent b01cf070b5
commit 7d4ea79376
31 changed files with 353 additions and 1463 deletions
+2
View File
@@ -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,
-1
View File
@@ -17,7 +17,6 @@ ignore:
- "**/lib/settings/*"
- "**/lib/signals/*"
- "**/lib/sol2/*"
- "**/lib/websocketpp/*"
- "**/lib/WinToast/*"
- "**/build*/"
- "**/ui_*.h"
-4
View File
@@ -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
+1
View File
@@ -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
-2
View File
@@ -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
-14
View File
@@ -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)
Submodule lib/websocketpp deleted from f1736a8e72
-145
View File
@@ -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
<bjoern@hoehrmann.de>. See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for
details.
The original license:
Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
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.
-1
View File
@@ -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());
-5
View File
@@ -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
@@ -1,11 +1,6 @@
#pragma once
#include "common/QLogging.hpp"
#include <QNetworkProxy>
#include <websocketpp/error.hpp>
#include <string>
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
@@ -13,7 +13,7 @@
namespace chatterino {
BttvLiveUpdateClient::BttvLiveUpdateClient(BttvLiveUpdates &manager)
: BasicPubSubClient<BttvLiveUpdateSubscription>(100)
: BasicPubSubClient(100)
, manager(manager)
{
}
@@ -8,7 +8,7 @@ namespace chatterino {
class BttvLiveUpdates;
class BttvLiveUpdateClient
: public BasicPubSubClient<BttvLiveUpdateSubscription>
: public BasicPubSubClient<BttvLiveUpdateSubscription, BttvLiveUpdateClient>
{
public:
BttvLiveUpdateClient(BttvLiveUpdates &manager);
@@ -20,7 +20,7 @@ namespace chatterino {
*
* @tparam Subscription see BasicPubSubManager
*/
template <typename SubscriptionT>
template <typename SubscriptionT, typename Derived>
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<Derived *>(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<Derived *>(this)->encodeUnsubscription(subscription);
this->ws_.sendText(encoded);
return true;
+2 -2
View File
@@ -14,7 +14,7 @@ namespace chatterino::seventv::eventapi {
Client::Client(SeventvEventAPI &manager,
std::chrono::milliseconds heartbeatInterval)
: BasicPubSubClient<Subscription>(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<Subscription>::onOpen();
BasicPubSubClient::onOpen();
this->lastHeartbeat_.store(std::chrono::steady_clock::now(),
std::memory_order::relaxed);
}
+2 -2
View File
@@ -18,8 +18,8 @@ struct Dispatch;
struct CosmeticCreateDispatch;
struct EntitlementCreateDeleteDispatch;
class Client : public BasicPubSubClient<Subscription>,
std::enable_shared_from_this<Client>
class Client : public BasicPubSubClient<Subscription, Client>,
public std::enable_shared_from_this<Client>
{
public:
Client(SeventvEventAPI &manager,
@@ -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 <websocketpp/common/cpp11.hpp>
#include <websocketpp/logger/basic.hpp>
#include <websocketpp/logger/levels.hpp>
#include <string>
namespace websocketpp::log {
template <typename concurrency, typename names>
class chatterinowebsocketpplogger : public basic<concurrency, names>
{
public:
using base = chatterinowebsocketpplogger<concurrency, names>;
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<concurrency, names> const &other)
: m_static_channels(other.m_static_channels)
, m_dynamic_channels(other.m_dynamic_channels)
{
}
#ifdef _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_
chatterinowebsocketpplogger<concurrency, names> &operator=(
chatterinowebsocketpplogger<concurrency, names> const &) = delete;
#endif // _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_
#ifdef _WEBSOCKETPP_MOVE_SEMANTICS_
/// Move constructor
chatterinowebsocketpplogger(
chatterinowebsocketpplogger<concurrency, names> &&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<concurrency, names> &operator=(
chatterinowebsocketpplogger<concurrency, names> &&) = 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
+140 -168
View File
@@ -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<boost::asio::steady_timer>(
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<PubSubMessageMessage>();
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<int64_t>(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<QString> 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<int64_t>(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<Listener> 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<PubSubCommunityPointsChannelV1Message>();
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
+46 -42
View File
@@ -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 <pajlada/signals/signal.hpp>
#include <QString>
@@ -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<chatterino::TopicData> {
std::size_t operator()(const chatterino::TopicData &data) const noexcept
{
return std::hash<QString>{}(data.topic);
}
};
class PubSubClient : public std::enable_shared_from_this<PubSubClient>
namespace chatterino {
struct PubSubMessage;
class PubSub;
struct PubSubListenMessage;
class PubSubClient : public BasicPubSubClient<TopicData, PubSubClient>
{
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<QString> topics;
QString nonce;
};
// The max amount of topics we may listen to with a single connection
static constexpr std::vector<QString>::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<Listener> 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<Listener> listeners_;
std::unordered_map<QString, NonceInfo> nonces_;
std::atomic<bool> awaitingPong_{false};
std::atomic<bool> started_{false};
std::shared_ptr<boost::asio::steady_timer> heartbeatTimer_;
const PubSubClientOptions &clientOptions_;
std::atomic<std::chrono::time_point<std::chrono::steady_clock>>
lastHeartbeat_;
std::chrono::milliseconds heartbeatInterval_;
bool isOpen_ = false;
PubSub &manager_;
};
} // namespace chatterino
@@ -1,14 +0,0 @@
#pragma once
#include <chrono>
namespace chatterino {
/**
* @brief Options to change the behaviour of the underlying websocket clients
**/
struct PubSubClientOptions {
std::chrono::seconds pingInterval_;
};
} // namespace chatterino
-53
View File
@@ -1,53 +0,0 @@
#pragma once
#include "common/QLogging.hpp"
#include <boost/asio.hpp>
#include <boost/asio/steady_timer.hpp>
#include <memory>
namespace chatterino {
class TwitchAccount;
struct ActionUser;
// Create timer using given ioService
template <typename Duration, typename Callback>
void runAfter(boost::asio::io_context &ioc, Duration duration, Callback cb)
{
auto timer = std::make_shared<boost::asio::steady_timer>(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 <typename Duration, typename Callback>
void runAfter(std::shared_ptr<boost::asio::steady_timer> 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
+52 -529
View File
@@ -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 <QJsonArray>
#include <QScopeGuard>
#include <algorithm>
#include <exception>
#include <memory>
#include <thread>
#include <utility>
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<PubSubManagerPrivate, PubSubClient>
{
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<PubSubClient> 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<PubSubManagerPrivate, PubSubClient>;
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<PubSubClient> 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<PubSubClient>(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<boost::asio::executor_work_guard<
boost::asio::io_context::executor_type>>(
this->websocketClient.get_io_service().get_executor());
this->thread = std::make_unique<std::thread>([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::NonceInfo> 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<PubSubMessageMessage>();
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<PubSubClient>(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<QString> 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<PubSubCommunityPointsChannelV1Message>();
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
+10 -101
View File
@@ -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 <boost/asio/io_context.hpp>
#include <boost/asio/ssl/context.hpp>
#include <pajlada/signals/signal.hpp>
#include <QJsonObject>
#include <QString>
#include <websocketpp/client.hpp>
#include <websocketpp/common/connection_hdl.hpp>
#include <websocketpp/common/memory.hpp>
#include <websocketpp/config/asio_client.hpp>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <thread>
#include <unordered_map>
#include <vector>
#if __has_include(<gtest/gtest_prod.h>)
# include <gtest/gtest_prod.h>
@@ -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<boost::asio::ssl::context>;
template <typename T>
using Signal =
pajlada::Signals::Signal<T>; // type-id is vector<T, Alloc<T>>
struct NonceInfo {
std::weak_ptr<PubSubClient> client;
QString messageType; // e.g. LISTEN or UNLISTEN
std::vector<QString> topics;
std::vector<QString>::size_type topicCount;
};
WebsocketClient websocketClient;
std::unique_ptr<std::thread> thread;
using Signal = pajlada::Signals::Signal<T>;
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<const QJsonObject &> redeemed;
} pointReward;
@@ -90,12 +52,8 @@ public:
* PubSub topic: community-points-channel-v1.{channelID}
*/
void listenToChannelPointRewards(const QString &channelID);
void unlistenChannelPointRewards();
struct {
std::atomic<uint32_t> connectionsClosed{0};
std::atomic<uint32_t> connectionsOpened{0};
std::atomic<uint32_t> connectionsFailed{0};
std::atomic<uint32_t> messagesReceived{0};
std::atomic<uint32_t> messagesFailedToParse{0};
std::atomic<uint32_t> failedListenResponses{0};
@@ -103,64 +61,15 @@ public:
std::atomic<uint32_t> 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<QString> requests;
std::atomic<bool> addingClient{false};
ExponentialBackoff<5> connectBackoff{std::chrono::milliseconds(1000)};
std::map<WebsocketHandle, std::shared_ptr<PubSubClient>,
std::owner_less<WebsocketHandle>>
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<NonceInfo> findNonceInfo(QString nonce);
std::unordered_map<QString, NonceInfo> nonces_;
void runThread();
std::shared_ptr<boost::asio::executor_work_guard<
boost::asio::io_context::executor_type>>
work{nullptr};
const QString host_;
const PubSubClientOptions clientOptions_;
OnceFlag stoppedFlag_;
bool stopping_{false};
std::unique_ptr<PubSubManagerPrivate> private_;
#ifdef FRIEND_TEST
friend class FTest;
-32
View File
@@ -1,32 +0,0 @@
#pragma once
#include "providers/twitch/ChatterinoWebSocketppLogger.hpp"
#include <websocketpp/client.hpp>
#include <websocketpp/config/asio_client.hpp>
#include <websocketpp/extensions/permessage_deflate/disabled.hpp>
#include <websocketpp/logger/basic.hpp>
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<chatterinoconfig>;
using WebsocketHandle = websocketpp::connection_hdl;
using WebsocketErrorCode = websocketpp::lib::error_code;
} // namespace chatterino
+2 -2
View File
@@ -18,9 +18,9 @@ PubSubMessage::PubSubMessage(QJsonObject _object)
}
}
std::optional<PubSubMessage> parsePubSubBaseMessage(const QString &blob)
std::optional<PubSubMessage> parsePubSubBaseMessage(const QByteArray &blob)
{
QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8()));
QJsonDocument jsonDoc(QJsonDocument::fromJson(blob));
if (jsonDoc.isNull())
{
+1 -1
View File
@@ -45,7 +45,7 @@ std::optional<InnerClass> PubSubMessage::toInner()
return InnerClass{this->nonce, data};
}
std::optional<PubSubMessage> parsePubSubBaseMessage(const QString &blob);
std::optional<PubSubMessage> parsePubSubBaseMessage(const QByteArray &blob);
} // namespace chatterino
+6 -10
View File
@@ -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()
+2 -2
View File
@@ -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
-3
View File
@@ -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",
+1 -1
View File
@@ -73,7 +73,7 @@ struct hash<DummySubscription> {
namespace {
class MyManager;
class MyClient : public BasicPubSubClient<DummySubscription>
class MyClient : public BasicPubSubClient<DummySubscription, MyClient>
{
public:
MyClient(MyManager &manager)
+69 -94
View File
@@ -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 <QString>
#include <QtCore/qtestsupport_core.h>
#include <chrono>
#include <mutex>
@@ -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