refactor(liveupdates): use WebSocketPool over websocketpp (#6308)
Reviewed-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
- Dev: Fix Arch Linux partial upgrade error in CI. (#6553)
|
||||
- Dev: Added Qt keyword and warning flags project wide. (#6520)
|
||||
- Def: Fixed compilation error in tests with Clang 21. (#6519)
|
||||
- Dev: The 7TV and BTTV liveupdates now use Boost.Beast's WebSockets. (#6308)
|
||||
- Dev: Fixed compilation warnings on clang-cl. (#6528)
|
||||
- Dev: Fixed compilation error in tests with Clang 21. (#6519)
|
||||
- Dev: Use CMake's `FetchContent` for RapidJSON, PajladaSignals, PajladaSerialize, and PajladaSettings. (#6560, #6567, #6569)
|
||||
|
||||
+3
-1
@@ -343,6 +343,8 @@ set(SOURCE_FILES
|
||||
providers/bttv/BttvLiveUpdates.cpp
|
||||
providers/bttv/BttvLiveUpdates.hpp
|
||||
|
||||
providers/bttv/liveupdates/BttvLiveUpdateClient.cpp
|
||||
providers/bttv/liveupdates/BttvLiveUpdateClient.hpp
|
||||
providers/bttv/liveupdates/BttvLiveUpdateMessages.cpp
|
||||
providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp
|
||||
providers/bttv/liveupdates/BttvLiveUpdateSubscription.cpp
|
||||
@@ -374,8 +376,8 @@ set(SOURCE_FILES
|
||||
providers/links/LinkResolver.hpp
|
||||
|
||||
providers/liveupdates/BasicPubSubClient.hpp
|
||||
providers/liveupdates/BasicPubSubListener.hpp
|
||||
providers/liveupdates/BasicPubSubManager.hpp
|
||||
providers/liveupdates/BasicPubSubWebsocket.hpp
|
||||
|
||||
providers/pronouns/Pronouns.cpp
|
||||
providers/pronouns/Pronouns.hpp
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
WebSocketPool::WebSocketPool() = default;
|
||||
WebSocketPool::WebSocketPool(QString shortName)
|
||||
: shortName(std::move(shortName)) {};
|
||||
|
||||
WebSocketPool::~WebSocketPool()
|
||||
{
|
||||
if (this->impl)
|
||||
@@ -34,7 +36,8 @@ WebSocketHandle WebSocketPool::createSocket(
|
||||
{
|
||||
try
|
||||
{
|
||||
this->impl = std::make_unique<ws::detail::WebSocketPoolImpl>();
|
||||
this->impl = std::make_unique<ws::detail::WebSocketPoolImpl>(
|
||||
this->shortName);
|
||||
}
|
||||
catch (const boost::system::system_error &err)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include <memory>
|
||||
@@ -74,7 +75,7 @@ struct WebSocketOptions {
|
||||
class WebSocketPool
|
||||
{
|
||||
public:
|
||||
WebSocketPool();
|
||||
WebSocketPool(QString shortName = {});
|
||||
~WebSocketPool();
|
||||
|
||||
[[nodiscard]] WebSocketHandle createSocket(
|
||||
@@ -82,6 +83,7 @@ public:
|
||||
|
||||
private:
|
||||
std::unique_ptr<ws::detail::WebSocketPoolImpl> impl;
|
||||
QString shortName;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
#include "util/RenameThread.hpp"
|
||||
|
||||
#include <boost/certify/https_verification.hpp>
|
||||
#include <QStringBuilder>
|
||||
|
||||
namespace chatterino::ws::detail {
|
||||
|
||||
WebSocketPoolImpl::WebSocketPoolImpl()
|
||||
WebSocketPoolImpl::WebSocketPoolImpl(const QString &shortName)
|
||||
: ioc(1)
|
||||
, ssl(boost::asio::ssl::context::tls_client)
|
||||
, work(this->ioc.get_executor())
|
||||
@@ -43,7 +44,15 @@ WebSocketPoolImpl::WebSocketPoolImpl()
|
||||
this->ioc.run();
|
||||
this->shutdownFlag.set();
|
||||
});
|
||||
renameThread(*this->ioThread, "WebSocketPool");
|
||||
|
||||
auto threadName = [&]() -> QString {
|
||||
if (shortName.isEmpty())
|
||||
{
|
||||
return "WebSocketPool";
|
||||
}
|
||||
return "WS-" % shortName;
|
||||
}();
|
||||
renameThread(*this->ioThread, threadName);
|
||||
}
|
||||
|
||||
WebSocketPoolImpl::~WebSocketPoolImpl()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <QString>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
@@ -17,7 +18,7 @@ class WebSocketConnection;
|
||||
class WebSocketPoolImpl
|
||||
{
|
||||
public:
|
||||
WebSocketPoolImpl();
|
||||
WebSocketPoolImpl(const QString &shortName);
|
||||
~WebSocketPoolImpl();
|
||||
|
||||
WebSocketPoolImpl(const WebSocketPoolImpl &) = delete;
|
||||
|
||||
@@ -1,105 +1,90 @@
|
||||
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||
|
||||
#include "common/Literals.hpp"
|
||||
#include "liveupdates/BttvLiveUpdateClient.hpp"
|
||||
#include "providers/liveupdates/BasicPubSubManager.hpp"
|
||||
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
using namespace chatterino::literals;
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
BttvLiveUpdates::BttvLiveUpdates(QString host)
|
||||
class BttvLiveUpdatesPrivate
|
||||
: public BasicPubSubManager<BttvLiveUpdatesPrivate, BttvLiveUpdateClient>
|
||||
{
|
||||
public:
|
||||
BttvLiveUpdatesPrivate(BttvLiveUpdates &parent, QString host);
|
||||
~BttvLiveUpdatesPrivate() override;
|
||||
BttvLiveUpdatesPrivate(const BttvLiveUpdatesPrivate &) = delete;
|
||||
BttvLiveUpdatesPrivate(const BttvLiveUpdatesPrivate &&) = delete;
|
||||
BttvLiveUpdatesPrivate &operator=(const BttvLiveUpdatesPrivate &) = delete;
|
||||
BttvLiveUpdatesPrivate &operator=(const BttvLiveUpdatesPrivate &&) = delete;
|
||||
|
||||
std::shared_ptr<BttvLiveUpdateClient> makeClient();
|
||||
|
||||
// Contains all joined Twitch channel-ids
|
||||
std::unordered_set<QString> joinedChannels;
|
||||
BttvLiveUpdates &parent;
|
||||
|
||||
friend BasicPubSubManager<BttvLiveUpdates, BttvLiveUpdateClient>;
|
||||
friend BttvLiveUpdates;
|
||||
};
|
||||
|
||||
BttvLiveUpdatesPrivate::BttvLiveUpdatesPrivate(BttvLiveUpdates &parent,
|
||||
QString host)
|
||||
: BasicPubSubManager(std::move(host), u"BTTV"_s)
|
||||
, parent(parent)
|
||||
{
|
||||
}
|
||||
|
||||
BttvLiveUpdates::~BttvLiveUpdates()
|
||||
BttvLiveUpdatesPrivate::~BttvLiveUpdatesPrivate()
|
||||
{
|
||||
this->stop();
|
||||
}
|
||||
|
||||
std::shared_ptr<BttvLiveUpdateClient> BttvLiveUpdatesPrivate::makeClient()
|
||||
{
|
||||
return std::make_shared<BttvLiveUpdateClient>(this->parent);
|
||||
}
|
||||
|
||||
BttvLiveUpdates::BttvLiveUpdates(QString host)
|
||||
: private_(std::make_unique<BttvLiveUpdatesPrivate>(*this, std::move(host)))
|
||||
{
|
||||
}
|
||||
|
||||
BttvLiveUpdates::~BttvLiveUpdates() = default;
|
||||
|
||||
void BttvLiveUpdates::joinChannel(const QString &channelID,
|
||||
const QString &userID)
|
||||
{
|
||||
if (this->joinedChannels_.insert(channelID).second)
|
||||
if (this->private_->joinedChannels.insert(channelID).second)
|
||||
{
|
||||
this->subscribe({BttvLiveUpdateSubscriptionChannel{channelID}});
|
||||
this->subscribe({BttvLiveUpdateBroadcastMe{.twitchID = channelID,
|
||||
.userID = userID}});
|
||||
this->private_->subscribe(
|
||||
{BttvLiveUpdateSubscriptionChannel{channelID}});
|
||||
this->private_->subscribe({BttvLiveUpdateBroadcastMe{
|
||||
.twitchID = channelID, .userID = userID}});
|
||||
}
|
||||
}
|
||||
|
||||
void BttvLiveUpdates::partChannel(const QString &id)
|
||||
{
|
||||
if (this->joinedChannels_.erase(id) > 0)
|
||||
if (this->private_->joinedChannels.erase(id) > 0)
|
||||
{
|
||||
this->unsubscribe({BttvLiveUpdateSubscriptionChannel{id}});
|
||||
this->private_->unsubscribe({BttvLiveUpdateSubscriptionChannel{id}});
|
||||
}
|
||||
}
|
||||
|
||||
void BttvLiveUpdates::onMessage(
|
||||
websocketpp::connection_hdl /*hdl*/,
|
||||
BasicPubSubManager<BttvLiveUpdateSubscription>::WebsocketMessagePtr msg)
|
||||
void BttvLiveUpdates::stop()
|
||||
{
|
||||
const auto &payload = QString::fromStdString(msg->get_payload());
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(payload.toUtf8()));
|
||||
this->private_->stop();
|
||||
}
|
||||
|
||||
if (jsonDoc.isNull())
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Failed to parse live update JSON";
|
||||
return;
|
||||
}
|
||||
auto json = jsonDoc.object();
|
||||
|
||||
auto eventType = json["name"].toString();
|
||||
auto eventData = json["data"].toObject();
|
||||
|
||||
if (eventType == "emote_create")
|
||||
{
|
||||
auto message = BttvLiveUpdateEmoteUpdateAddMessage(eventData);
|
||||
|
||||
if (!message.validate())
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Invalid add message" << json;
|
||||
return;
|
||||
}
|
||||
|
||||
this->signals_.emoteAdded.invoke(message);
|
||||
}
|
||||
else if (eventType == "emote_update")
|
||||
{
|
||||
auto message = BttvLiveUpdateEmoteUpdateAddMessage(eventData);
|
||||
|
||||
if (!message.validate())
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Invalid update message" << json;
|
||||
return;
|
||||
}
|
||||
|
||||
this->signals_.emoteUpdated.invoke(message);
|
||||
}
|
||||
else if (eventType == "emote_delete")
|
||||
{
|
||||
auto message = BttvLiveUpdateEmoteRemoveMessage(eventData);
|
||||
|
||||
if (!message.validate())
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Invalid deletion message" << json;
|
||||
return;
|
||||
}
|
||||
|
||||
this->signals_.emoteRemoved.invoke(message);
|
||||
}
|
||||
else if (eventType == "lookup_user")
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Unhandled event:" << json;
|
||||
}
|
||||
const liveupdates::Diag &BttvLiveUpdates::diag() const
|
||||
{
|
||||
return this->private_->diag;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp"
|
||||
#include "providers/liveupdates/BasicPubSubManager.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
#include <QString>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class BttvLiveUpdates : public BasicPubSubManager<BttvLiveUpdateSubscription>
|
||||
namespace liveupdates {
|
||||
struct Diag;
|
||||
} // namespace liveupdates
|
||||
|
||||
struct BttvLiveUpdateEmoteUpdateAddMessage;
|
||||
struct BttvLiveUpdateEmoteRemoveMessage;
|
||||
|
||||
class BttvLiveUpdatesPrivate;
|
||||
class BttvLiveUpdates
|
||||
{
|
||||
template <typename T>
|
||||
using Signal =
|
||||
pajlada::Signals::Signal<T>; // type-id is vector<T, Alloc<T>>
|
||||
using Signal = pajlada::Signals::Signal<T>;
|
||||
|
||||
public:
|
||||
BttvLiveUpdates(QString host);
|
||||
~BttvLiveUpdates() override;
|
||||
~BttvLiveUpdates();
|
||||
|
||||
struct {
|
||||
Signal<BttvLiveUpdateEmoteUpdateAddMessage> emoteAdded;
|
||||
@@ -44,15 +47,19 @@ public:
|
||||
*/
|
||||
void partChannel(const QString &id);
|
||||
|
||||
protected:
|
||||
void onMessage(
|
||||
websocketpp::connection_hdl hdl,
|
||||
BasicPubSubManager<BttvLiveUpdateSubscription>::WebsocketMessagePtr msg)
|
||||
override;
|
||||
/// Stop the manager
|
||||
///
|
||||
/// Used in tests to check that connections are closed (through #diag()).
|
||||
/// Otherwise, calling the destructor is sufficient.
|
||||
void stop();
|
||||
|
||||
/// Statistics about the opened/closed connections and received messages
|
||||
///
|
||||
/// Used in tests.
|
||||
const liveupdates::Diag &diag() const;
|
||||
|
||||
private:
|
||||
// Contains all joined Twitch channel-ids
|
||||
std::unordered_set<QString> joinedChannels_;
|
||||
std::unique_ptr<BttvLiveUpdatesPrivate> private_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateClient.hpp"
|
||||
|
||||
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
BttvLiveUpdateClient::BttvLiveUpdateClient(BttvLiveUpdates &manager)
|
||||
: BasicPubSubClient<BttvLiveUpdateSubscription>(100)
|
||||
, manager(manager)
|
||||
{
|
||||
}
|
||||
|
||||
void BttvLiveUpdateClient::onMessage(const QByteArray &msg)
|
||||
{
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(msg));
|
||||
|
||||
if (jsonDoc.isNull())
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Failed to parse live update JSON";
|
||||
return;
|
||||
}
|
||||
auto json = jsonDoc.object();
|
||||
|
||||
auto eventType = json["name"].toString();
|
||||
auto eventData = json["data"].toObject();
|
||||
|
||||
if (eventType == "emote_create")
|
||||
{
|
||||
auto message = BttvLiveUpdateEmoteUpdateAddMessage(eventData);
|
||||
|
||||
if (!message.validate())
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Invalid add message" << json;
|
||||
return;
|
||||
}
|
||||
|
||||
this->manager.signals_.emoteAdded.invoke(message);
|
||||
}
|
||||
else if (eventType == "emote_update")
|
||||
{
|
||||
auto message = BttvLiveUpdateEmoteUpdateAddMessage(eventData);
|
||||
|
||||
if (!message.validate())
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Invalid update message" << json;
|
||||
return;
|
||||
}
|
||||
|
||||
this->manager.signals_.emoteUpdated.invoke(message);
|
||||
}
|
||||
else if (eventType == "emote_delete")
|
||||
{
|
||||
auto message = BttvLiveUpdateEmoteRemoveMessage(eventData);
|
||||
|
||||
if (!message.validate())
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Invalid deletion message" << json;
|
||||
return;
|
||||
}
|
||||
|
||||
this->manager.signals_.emoteRemoved.invoke(message);
|
||||
}
|
||||
else if (eventType == "lookup_user")
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Unhandled event:" << json;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp"
|
||||
#include "providers/liveupdates/BasicPubSubClient.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class BttvLiveUpdates;
|
||||
|
||||
class BttvLiveUpdateClient
|
||||
: public BasicPubSubClient<BttvLiveUpdateSubscription>
|
||||
{
|
||||
public:
|
||||
BttvLiveUpdateClient(BttvLiveUpdates &manager);
|
||||
|
||||
void onMessage(const QByteArray &msg) /* override */;
|
||||
|
||||
private:
|
||||
BttvLiveUpdates &manager;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -1,15 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/QLogging.hpp"
|
||||
#include "providers/liveupdates/BasicPubSubWebsocket.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "common/websockets/WebSocketPool.hpp"
|
||||
#include "debug/AssertInGuiThread.hpp"
|
||||
#include "util/DebugCount.hpp"
|
||||
#include "util/Helpers.hpp"
|
||||
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace chatterino {
|
||||
@@ -25,51 +20,48 @@ namespace chatterino {
|
||||
*
|
||||
* @tparam Subscription see BasicPubSubManager
|
||||
*/
|
||||
template <typename Subscription>
|
||||
template <typename SubscriptionT>
|
||||
class BasicPubSubClient
|
||||
: public std::enable_shared_from_this<BasicPubSubClient<Subscription>>
|
||||
{
|
||||
public:
|
||||
using Subscription = SubscriptionT;
|
||||
|
||||
// The maximum amount of subscriptions this connections can handle
|
||||
const size_t maxSubscriptions;
|
||||
|
||||
BasicPubSubClient(liveupdates::WebsocketClient &websocketClient,
|
||||
liveupdates::WebsocketHandle handle,
|
||||
size_t maxSubscriptions = 100)
|
||||
BasicPubSubClient(size_t maxSubscriptions = 100)
|
||||
: maxSubscriptions(maxSubscriptions)
|
||||
, websocketClient_(websocketClient)
|
||||
, handle_(std::move(handle))
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~BasicPubSubClient() = default;
|
||||
|
||||
~BasicPubSubClient() = default;
|
||||
BasicPubSubClient(const BasicPubSubClient &) = delete;
|
||||
BasicPubSubClient(const BasicPubSubClient &&) = delete;
|
||||
BasicPubSubClient &operator=(const BasicPubSubClient &) = delete;
|
||||
BasicPubSubClient &operator=(const BasicPubSubClient &&) = delete;
|
||||
|
||||
/// The websocket handshake completed.
|
||||
///
|
||||
/// Called from the manager in the GUI thread.
|
||||
void onOpen()
|
||||
{
|
||||
assertInGuiThread();
|
||||
this->open_ = true;
|
||||
}
|
||||
|
||||
/// A message has been received.
|
||||
///
|
||||
/// Called from the websocket thread.
|
||||
void onMessage(const QByteArray & /*msg*/)
|
||||
{
|
||||
}
|
||||
|
||||
void close()
|
||||
{
|
||||
this->ws_.close();
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void onConnectionEstablished()
|
||||
{
|
||||
}
|
||||
|
||||
bool send(const char *payload)
|
||||
{
|
||||
liveupdates::WebsocketErrorCode ec;
|
||||
this->websocketClient_.send(this->handle_, payload,
|
||||
websocketpp::frame::opcode::text, ec);
|
||||
|
||||
if (ec)
|
||||
{
|
||||
qCDebug(chatterinoLiveupdates) << "Error sending message" << payload
|
||||
<< ":" << ec.message().c_str();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this client subscribed to this subscription
|
||||
* and the current subscriptions don't exceed the maximum
|
||||
@@ -97,7 +89,7 @@ protected:
|
||||
DebugCount::increase("LiveUpdates subscriptions");
|
||||
|
||||
QByteArray encoded = subscription.encodeSubscribe();
|
||||
this->send(encoded);
|
||||
this->ws_.sendText(encoded);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -117,72 +109,23 @@ protected:
|
||||
DebugCount::decrease("LiveUpdates subscriptions");
|
||||
|
||||
QByteArray encoded = subscription.encodeUnsubscribe();
|
||||
this->send(encoded);
|
||||
this->ws_.sendText(encoded);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void close(const std::string &reason,
|
||||
websocketpp::close::status::value code =
|
||||
websocketpp::close::status::normal)
|
||||
bool isOpen() const
|
||||
{
|
||||
liveupdates::WebsocketErrorCode ec;
|
||||
|
||||
auto conn = this->websocketClient_.get_con_from_hdl(this->handle_, ec);
|
||||
if (ec)
|
||||
{
|
||||
qCDebug(chatterinoLiveupdates)
|
||||
<< "Error getting connection:" << ec.message().c_str();
|
||||
return;
|
||||
}
|
||||
|
||||
conn->close(code, reason, ec);
|
||||
if (ec)
|
||||
{
|
||||
qCDebug(chatterinoLiveupdates)
|
||||
<< "Error closing:" << ec.message().c_str();
|
||||
return;
|
||||
}
|
||||
return this->open_;
|
||||
}
|
||||
|
||||
bool isStarted() const
|
||||
{
|
||||
return this->started_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Will be called when the clients has been requested to stop
|
||||
*
|
||||
* Derived classes can override this to implement their own shutdown behaviour
|
||||
*/
|
||||
virtual void stopImpl()
|
||||
{
|
||||
}
|
||||
|
||||
liveupdates::WebsocketClient &websocketClient_;
|
||||
|
||||
private:
|
||||
void start()
|
||||
{
|
||||
assert(!this->isStarted());
|
||||
this->started_.store(true, std::memory_order_release);
|
||||
this->onConnectionEstablished();
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
assert(this->isStarted());
|
||||
this->started_.store(false, std::memory_order_release);
|
||||
|
||||
this->stopImpl();
|
||||
}
|
||||
|
||||
liveupdates::WebsocketHandle handle_;
|
||||
WebSocketHandle ws_;
|
||||
std::unordered_set<Subscription> subscriptions_;
|
||||
|
||||
std::atomic<bool> started_{false};
|
||||
bool open_ = false;
|
||||
|
||||
template <typename ManagerSubscription>
|
||||
template <typename ManagerSubscription, typename ManagerClient>
|
||||
friend class BasicPubSubManager;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/websockets/WebSocketPool.hpp"
|
||||
#include "util/PostToThread.hpp"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QPointer>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
template <typename Manager>
|
||||
struct BasicPubSubListener : public WebSocketListener {
|
||||
BasicPubSubListener(std::weak_ptr<typename Manager::Client> client,
|
||||
QPointer<Manager> manager, size_t id)
|
||||
: client(std::move(client))
|
||||
, manager(std::move(manager))
|
||||
, id(id)
|
||||
{
|
||||
}
|
||||
|
||||
void onOpen() override
|
||||
{
|
||||
runInGuiThread([manager = this->manager, id = this->id] {
|
||||
if (manager)
|
||||
{
|
||||
manager->onConnectionOpen(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onTextMessage(QByteArray msg) override
|
||||
{
|
||||
auto sp = this->client.lock();
|
||||
if (sp)
|
||||
{
|
||||
sp->onMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void onBinaryMessage(QByteArray msg) override
|
||||
{
|
||||
auto sp = this->client.lock();
|
||||
if (sp)
|
||||
{
|
||||
sp->onMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void onClose(std::unique_ptr<WebSocketListener> /* self */) override
|
||||
{
|
||||
runInGuiThread([manager = this->manager, id = this->id] {
|
||||
if (manager)
|
||||
{
|
||||
manager->onConnectionClose(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::weak_ptr<typename Manager::Client> client;
|
||||
QPointer<Manager> manager;
|
||||
size_t id;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -1,105 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/QLogging.hpp"
|
||||
#include "common/Version.hpp"
|
||||
#include "providers/liveupdates/BasicPubSubClient.hpp"
|
||||
#include "providers/liveupdates/BasicPubSubWebsocket.hpp"
|
||||
#include "providers/NetworkConfigurationProvider.hpp"
|
||||
#include "providers/twitch/PubSubHelpers.hpp"
|
||||
#include "common/websockets/WebSocketPool.hpp"
|
||||
#include "providers/liveupdates/BasicPubSubListener.hpp"
|
||||
#include "providers/liveupdates/Diag.hpp"
|
||||
#include "util/DebugCount.hpp"
|
||||
#include "util/ExponentialBackoff.hpp"
|
||||
#include "util/OnceFlag.hpp"
|
||||
#include "util/RenameThread.hpp"
|
||||
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
#include <QJsonObject>
|
||||
#include <QScopeGuard>
|
||||
#include <QString>
|
||||
#include <QStringBuilder>
|
||||
#include <websocketpp/client.hpp>
|
||||
#include <QPointer>
|
||||
#include <QTimer>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace liveupdates {
|
||||
|
||||
template <typename Manager, typename Client>
|
||||
concept IsManager = requires(Manager &manager) {
|
||||
{ manager.makeClient() } -> std::same_as<std::shared_ptr<Client>>;
|
||||
};
|
||||
|
||||
template <typename Client>
|
||||
concept IsClient = requires(Client &client, const QByteArray &msg) {
|
||||
{ client.onOpen() } -> std::same_as<void>;
|
||||
{ client.onMessage(msg) } -> std::same_as<void>;
|
||||
{ client.close() } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
} // namespace liveupdates
|
||||
|
||||
/**
|
||||
* This class is the basis for connecting and interacting with
|
||||
* simple PubSub servers over the Websocket protocol.
|
||||
* It acts as a pool for connections (see BasicPubSubClient).
|
||||
*
|
||||
* You can customize the clients, by creating your custom
|
||||
* client in ::createClient.
|
||||
*
|
||||
* You **must** implement #onMessage. The method gets called for every
|
||||
* received message on every connection.
|
||||
* If you want to get the connection this message was received on,
|
||||
* use #findClient.
|
||||
* You **must** implement a method `makeClient()` that returns a shared pointer
|
||||
* of the client.
|
||||
*
|
||||
* You must expose your own subscribe and unsubscribe methods
|
||||
* (e.g. [un-]subscribeTopic).
|
||||
* This manager does not keep track of the subscriptions.
|
||||
*
|
||||
* @tparam Subscription
|
||||
* The subscription has the following requirements:
|
||||
* It must have the methods QByteArray encodeSubscribe(),
|
||||
* and QByteArray encodeUnsubscribe().
|
||||
* It must have an overload for
|
||||
* QDebug &operator<< (see tests/src/BasicPubSub.cpp),
|
||||
* a specialization for std::hash,
|
||||
* and and overload for operator== and operator!=.
|
||||
* @tparam Derived
|
||||
* The derived class. Used to dispatch to makeClient().
|
||||
*
|
||||
* @tparam ClientT
|
||||
* The client type. Must confirm to the IsClient above. Use BasicPubSubClient
|
||||
* for a common implementation. Used to dispatch to the correct methods there
|
||||
* and to get the subscription type.
|
||||
*
|
||||
* @see BasicPubSubClient
|
||||
*/
|
||||
template <typename Subscription>
|
||||
class BasicPubSubManager
|
||||
template <typename Derived, typename ClientT>
|
||||
class BasicPubSubManager : public QObject
|
||||
{
|
||||
public:
|
||||
using Subscription = ClientT::Subscription;
|
||||
using Client = ClientT;
|
||||
|
||||
BasicPubSubManager(QString host, QString shortName)
|
||||
: host_(std::move(host))
|
||||
, shortName_(std::move(shortName))
|
||||
: pool_(std::make_optional<WebSocketPool>(shortName))
|
||||
, host_(std::move(host))
|
||||
{
|
||||
this->websocketClient_.set_access_channels(
|
||||
websocketpp::log::alevel::all);
|
||||
this->websocketClient_.clear_access_channels(
|
||||
websocketpp::log::alevel::frame_payload |
|
||||
websocketpp::log::alevel::frame_header);
|
||||
|
||||
this->websocketClient_.init_asio();
|
||||
|
||||
// SSL Handshake
|
||||
this->websocketClient_.set_tls_init_handler([this](auto hdl) {
|
||||
return this->onTLSInit(hdl);
|
||||
});
|
||||
|
||||
this->websocketClient_.set_message_handler([this](auto hdl, auto msg) {
|
||||
this->onMessage(hdl, msg);
|
||||
});
|
||||
this->websocketClient_.set_open_handler([this](auto hdl) {
|
||||
this->onConnectionOpen(hdl);
|
||||
});
|
||||
this->websocketClient_.set_close_handler([this](auto hdl) {
|
||||
this->onConnectionClose(hdl);
|
||||
});
|
||||
this->websocketClient_.set_fail_handler([this](auto hdl) {
|
||||
this->onConnectionFail(hdl);
|
||||
});
|
||||
this->websocketClient_.set_user_agent(
|
||||
QStringLiteral("Chatterino/%1 (%2)")
|
||||
.arg(Version::instance().version(),
|
||||
Version::instance().commitHash())
|
||||
.toStdString());
|
||||
// We do this here, because `Derived` needs to be a complete type. If we
|
||||
// did it as a requires clause on the class, the type would be
|
||||
// incomplete.
|
||||
static_assert(liveupdates::IsManager<Derived, Client>);
|
||||
static_assert(liveupdates::IsClient<Client>);
|
||||
}
|
||||
|
||||
virtual ~BasicPubSubManager()
|
||||
~BasicPubSubManager() override
|
||||
{
|
||||
// The derived class must call stop in its destructor
|
||||
assert(this->stopping_);
|
||||
@@ -111,28 +87,7 @@ public:
|
||||
BasicPubSubManager &operator=(const BasicPubSubManager &&) = delete;
|
||||
|
||||
/** This is only used for testing. */
|
||||
struct {
|
||||
std::atomic<uint32_t> connectionsClosed{0};
|
||||
std::atomic<uint32_t> connectionsOpened{0};
|
||||
std::atomic<uint32_t> connectionsFailed{0};
|
||||
} diag;
|
||||
|
||||
void start()
|
||||
{
|
||||
this->work_ = std::make_shared<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>(
|
||||
this->websocketClient_.get_io_service().get_executor());
|
||||
this->mainThread_.reset(new std::thread([this] {
|
||||
// make sure we set in any case, even exceptions
|
||||
auto guard = qScopeGuard([&] {
|
||||
this->stoppedFlag_.set();
|
||||
});
|
||||
|
||||
runThread();
|
||||
}));
|
||||
|
||||
renameThread(*this->mainThread_.get(), "BPSM-" % this->shortName_);
|
||||
}
|
||||
liveupdates::Diag diag;
|
||||
|
||||
void stop()
|
||||
{
|
||||
@@ -142,74 +97,10 @@ public:
|
||||
}
|
||||
|
||||
this->stopping_ = true;
|
||||
|
||||
for (const auto &client : this->clients_)
|
||||
{
|
||||
client.second->close("Shutting down");
|
||||
}
|
||||
|
||||
this->work_.reset();
|
||||
|
||||
if (!this->mainThread_->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.
|
||||
if (this->stoppedFlag_.waitFor(std::chrono::milliseconds{100}))
|
||||
{
|
||||
this->mainThread_->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->mainThread_->join();
|
||||
return;
|
||||
}
|
||||
|
||||
qCWarning(chatterinoLiveupdates)
|
||||
<< "Thread didn't finish after stopping";
|
||||
this->pool_.reset();
|
||||
}
|
||||
|
||||
protected:
|
||||
using WebsocketMessagePtr =
|
||||
websocketpp::config::asio_tls_client::message_type::ptr;
|
||||
using WebsocketContextPtr =
|
||||
websocketpp::lib::shared_ptr<boost::asio::ssl::context>;
|
||||
|
||||
virtual void onMessage(websocketpp::connection_hdl hdl,
|
||||
WebsocketMessagePtr msg) = 0;
|
||||
|
||||
virtual std::shared_ptr<BasicPubSubClient<Subscription>> createClient(
|
||||
liveupdates::WebsocketClient &client, websocketpp::connection_hdl hdl)
|
||||
{
|
||||
return std::make_shared<BasicPubSubClient<Subscription>>(client, hdl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param hdl The handle of the client.
|
||||
* @return The client managing this connection, empty shared_ptr otherwise.
|
||||
*/
|
||||
std::shared_ptr<BasicPubSubClient<Subscription>> findClient(
|
||||
websocketpp::connection_hdl hdl)
|
||||
{
|
||||
auto clientIt = this->clients_.find(hdl);
|
||||
|
||||
if (clientIt == this->clients_.end())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return clientIt->second;
|
||||
}
|
||||
|
||||
void unsubscribe(const Subscription &subscription)
|
||||
{
|
||||
for (auto &client : this->clients_)
|
||||
@@ -233,8 +124,18 @@ protected:
|
||||
DebugCount::increase("LiveUpdates subscription backlog");
|
||||
}
|
||||
|
||||
const std::unordered_map<size_t, std::shared_ptr<Client>> &clients() const
|
||||
{
|
||||
return this->clients_;
|
||||
}
|
||||
|
||||
private:
|
||||
void onConnectionOpen(websocketpp::connection_hdl hdl)
|
||||
Derived *derived()
|
||||
{
|
||||
return static_cast<Derived *>(this);
|
||||
}
|
||||
|
||||
void onConnectionOpen(size_t id)
|
||||
{
|
||||
DebugCount::increase("LiveUpdates connections");
|
||||
this->addingClient_ = false;
|
||||
@@ -242,14 +143,8 @@ private:
|
||||
|
||||
this->connectBackoff_.reset();
|
||||
|
||||
auto client = this->createClient(this->websocketClient_, hdl);
|
||||
|
||||
// We separate the starting from the constructor because we will want to use
|
||||
// shared_from_this
|
||||
client->start();
|
||||
|
||||
this->clients_.emplace(hdl, client);
|
||||
|
||||
auto *client = resolve(id);
|
||||
client->onOpen();
|
||||
auto pendingSubsToTake = std::min(this->pendingSubscriptions_.size(),
|
||||
client->maxSubscriptions);
|
||||
|
||||
@@ -278,92 +173,64 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void onConnectionFail(websocketpp::connection_hdl hdl)
|
||||
void onConnectionClose(size_t id)
|
||||
{
|
||||
DebugCount::increase("LiveUpdates failed connections");
|
||||
this->diag.connectionsFailed.fetch_add(1, std::memory_order_acq_rel);
|
||||
this->addingClient_ = false;
|
||||
|
||||
if (auto conn = this->websocketClient_.get_con_from_hdl(std::move(hdl)))
|
||||
auto it = this->clients_.find(id);
|
||||
if (it == this->clients_.end())
|
||||
{
|
||||
qCDebug(chatterinoLiveupdates)
|
||||
<< "LiveUpdates connection attempt failed (error: "
|
||||
<< conn->get_ec().message().c_str() << ")";
|
||||
qCWarning(chatterinoLiveupdates) << "Unknown client:" << id;
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
DebugCount::decrease("LiveUpdates connections");
|
||||
qCDebug(chatterinoLiveupdates) << "Connection" << id << "closed";
|
||||
|
||||
auto subs = std::move(it->second->subscriptions_);
|
||||
bool wasOpen = it->second->isOpen();
|
||||
|
||||
if (wasOpen)
|
||||
{
|
||||
this->diag.connectionsClosed.fetch_add(1,
|
||||
std::memory_order::relaxed);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoLiveupdates)
|
||||
<< "LiveUpdates connection attempt failed but we can't get the "
|
||||
"connection from a handle.";
|
||||
this->diag.connectionsFailed.fetch_add(1,
|
||||
std::memory_order::relaxed);
|
||||
}
|
||||
this->addingClient_ = false;
|
||||
if (!this->pendingSubscriptions_.empty())
|
||||
|
||||
this->clients_.erase(it);
|
||||
if (this->stopping_)
|
||||
{
|
||||
runAfter(this->websocketClient_.get_io_service(),
|
||||
this->connectBackoff_.next(), [this](auto /*timer*/) {
|
||||
this->addClient();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void onConnectionClose(websocketpp::connection_hdl hdl)
|
||||
{
|
||||
qCDebug(chatterinoLiveupdates) << "Connection closed";
|
||||
DebugCount::decrease("LiveUpdates connections");
|
||||
this->diag.connectionsClosed.fetch_add(1, std::memory_order_acq_rel);
|
||||
|
||||
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_)
|
||||
if (!wasOpen)
|
||||
{
|
||||
for (const auto &sub : client->subscriptions_)
|
||||
{
|
||||
this->subscribe(sub);
|
||||
}
|
||||
qCWarning(chatterinoLiveupdates)
|
||||
<< "Retrying after" << id << "failed";
|
||||
this->pendingSubscriptions_.insert(
|
||||
this->pendingSubscriptions_.end(),
|
||||
std::make_move_iterator(subs.begin()),
|
||||
std::make_move_iterator(subs.end()));
|
||||
QTimer::singleShot(this->connectBackoff_.next(), this, [this] {
|
||||
this->addClient();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
WebsocketContextPtr onTLSInit(const websocketpp::connection_hdl & /*hdl*/)
|
||||
{
|
||||
WebsocketContextPtr ctx(new boost::asio::ssl::context(
|
||||
boost::asio::ssl::context::tls_client));
|
||||
|
||||
try
|
||||
for (const auto &sub : subs)
|
||||
{
|
||||
ctx->set_options(boost::asio::ssl::context::default_workarounds |
|
||||
boost::asio::ssl::context::no_tlsv1 |
|
||||
boost::asio::ssl::context::no_tlsv1_1 |
|
||||
boost::asio::ssl::context::single_dh_use);
|
||||
this->subscribe(sub);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
qCDebug(chatterinoLiveupdates)
|
||||
<< "Exception caught in OnTLSInit:" << e.what();
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void runThread()
|
||||
{
|
||||
qCDebug(chatterinoLiveupdates) << "Start LiveUpdates manager thread";
|
||||
this->websocketClient_.run();
|
||||
qCDebug(chatterinoLiveupdates)
|
||||
<< "Done with LiveUpdates manager thread";
|
||||
}
|
||||
|
||||
void addClient()
|
||||
{
|
||||
if (this->addingClient_)
|
||||
if (this->addingClient_ || !this->pool_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -372,20 +239,17 @@ private:
|
||||
|
||||
this->addingClient_ = true;
|
||||
|
||||
websocketpp::lib::error_code ec;
|
||||
auto con = this->websocketClient_.get_connection(
|
||||
this->host_.toStdString(), ec);
|
||||
|
||||
if (ec)
|
||||
{
|
||||
qCDebug(chatterinoLiveupdates)
|
||||
<< "Unable to establish connection:" << ec.message().c_str();
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkConfigurationProvider::applyToWebSocket(con);
|
||||
|
||||
this->websocketClient_.connect(con);
|
||||
auto id = this->nextId_++;
|
||||
auto client = this->derived()->makeClient();
|
||||
auto hdl = this->pool_->createSocket(
|
||||
WebSocketOptions{
|
||||
.url = this->host_,
|
||||
.headers = {},
|
||||
},
|
||||
std::make_unique<BasicPubSubListener<Derived>>(
|
||||
std::weak_ptr{client}, this->derived(), id));
|
||||
client->ws_ = std::move(hdl);
|
||||
this->clients_.emplace(id, std::move(client));
|
||||
}
|
||||
|
||||
bool trySubscribe(const Subscription &subscription)
|
||||
@@ -400,29 +264,30 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
Client *resolve(size_t id)
|
||||
{
|
||||
auto it = this->clients_.find(id);
|
||||
if (it == this->clients_.end())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
std::vector<Subscription> pendingSubscriptions_;
|
||||
std::atomic<bool> addingClient_{false};
|
||||
ExponentialBackoff<5> connectBackoff_{std::chrono::milliseconds(1000)};
|
||||
|
||||
liveupdates::WebsocketClient websocketClient_;
|
||||
std::unique_ptr<std::thread> mainThread_;
|
||||
OnceFlag stoppedFlag_;
|
||||
|
||||
std::map<liveupdates::WebsocketHandle,
|
||||
std::shared_ptr<BasicPubSubClient<Subscription>>,
|
||||
std::owner_less<liveupdates::WebsocketHandle>>
|
||||
clients_;
|
||||
|
||||
std::shared_ptr<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>
|
||||
work_{nullptr};
|
||||
std::optional<WebSocketPool> pool_;
|
||||
std::unordered_map<size_t, std::shared_ptr<Client>> clients_;
|
||||
|
||||
const QString host_;
|
||||
|
||||
/// Short name of the service (e.g. "7TV" or "BTTV")
|
||||
const QString shortName_;
|
||||
size_t nextId_ = 0;
|
||||
|
||||
bool stopping_{false};
|
||||
bool stopping_ = false;
|
||||
bool addingClient_ = false;
|
||||
|
||||
friend BasicPubSubListener<Derived>;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,36 +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 BasicPubSubConfig : public websocketpp::config::asio_tls_client {
|
||||
// NOLINTBEGIN(modernize-use-using)
|
||||
typedef websocketpp::log::chatterinowebsocketpplogger<
|
||||
concurrency_type, websocketpp::log::elevel>
|
||||
elog_type;
|
||||
typedef websocketpp::log::chatterinowebsocketpplogger<
|
||||
concurrency_type, websocketpp::log::alevel>
|
||||
alog_type;
|
||||
|
||||
struct PerMessageDeflateConfig {
|
||||
};
|
||||
|
||||
typedef websocketpp::extensions::permessage_deflate::disabled<
|
||||
PerMessageDeflateConfig>
|
||||
permessage_deflate_type;
|
||||
// NOLINTEND(modernize-use-using)
|
||||
};
|
||||
|
||||
namespace liveupdates {
|
||||
using WebsocketClient = websocketpp::client<chatterino::BasicPubSubConfig>;
|
||||
using WebsocketHandle = websocketpp::connection_hdl;
|
||||
using WebsocketErrorCode = websocketpp::lib::error_code;
|
||||
} // namespace liveupdates
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace chatterino::liveupdates {
|
||||
|
||||
struct Diag {
|
||||
std::atomic<uint32_t> connectionsClosed{0};
|
||||
std::atomic<uint32_t> connectionsOpened{0};
|
||||
std::atomic<uint32_t> connectionsFailed{0};
|
||||
};
|
||||
|
||||
} // namespace chatterino::liveupdates
|
||||
@@ -1,13 +1,8 @@
|
||||
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Literals.hpp"
|
||||
#include "providers/liveupdates/BasicPubSubManager.hpp"
|
||||
#include "providers/seventv/eventapi/Client.hpp"
|
||||
#include "providers/seventv/eventapi/Dispatch.hpp"
|
||||
#include "providers/seventv/eventapi/Message.hpp"
|
||||
#include "providers/seventv/SeventvBadges.hpp"
|
||||
#include "providers/seventv/SeventvCosmetics.hpp"
|
||||
#include "util/QMagicEnum.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
|
||||
@@ -17,49 +12,118 @@ namespace chatterino {
|
||||
|
||||
using namespace seventv;
|
||||
using namespace seventv::eventapi;
|
||||
using namespace chatterino::literals;
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
SeventvEventAPI::SeventvEventAPI(
|
||||
QString host, std::chrono::milliseconds defaultHeartbeatInterval)
|
||||
: BasicPubSubManager(std::move(host), u"7TV"_s)
|
||||
, heartbeatInterval_(defaultHeartbeatInterval)
|
||||
class SeventvEventAPIPrivate
|
||||
: public BasicPubSubManager<SeventvEventAPIPrivate,
|
||||
seventv::eventapi::Client>
|
||||
{
|
||||
public:
|
||||
SeventvEventAPIPrivate(SeventvEventAPI &parent, QString host,
|
||||
std::chrono::milliseconds defaultHeartbeatInterval);
|
||||
~SeventvEventAPIPrivate() override;
|
||||
SeventvEventAPIPrivate(const SeventvEventAPIPrivate &) = delete;
|
||||
SeventvEventAPIPrivate(SeventvEventAPIPrivate &&) = delete;
|
||||
SeventvEventAPIPrivate &operator=(const SeventvEventAPIPrivate &) = delete;
|
||||
SeventvEventAPIPrivate &operator=(SeventvEventAPIPrivate &&) = delete;
|
||||
|
||||
std::shared_ptr<seventv::eventapi::Client> makeClient();
|
||||
void checkHeartbeats();
|
||||
|
||||
/** emote-set ids */
|
||||
std::unordered_set<QString> subscribedEmoteSets;
|
||||
/** user ids */
|
||||
std::unordered_set<QString> subscribedUsers;
|
||||
/** Twitch channel ids */
|
||||
std::unordered_set<QString> subscribedTwitchChannels;
|
||||
|
||||
std::chrono::milliseconds heartbeatInterval;
|
||||
QTimer heartbeatTimer;
|
||||
|
||||
SeventvEventAPI &parent;
|
||||
|
||||
friend BasicPubSubManager<SeventvEventAPI, seventv::eventapi::Client>;
|
||||
friend SeventvEventAPI;
|
||||
};
|
||||
|
||||
SeventvEventAPIPrivate::SeventvEventAPIPrivate(
|
||||
SeventvEventAPI &parent, QString host,
|
||||
std::chrono::milliseconds defaultHeartbeatInterval)
|
||||
: BasicPubSubManager(std::move(host), u"7TV"_s)
|
||||
, heartbeatInterval(defaultHeartbeatInterval)
|
||||
, parent(parent)
|
||||
{
|
||||
QObject::connect(&this->heartbeatTimer, &QTimer::timeout, this,
|
||||
&SeventvEventAPIPrivate::checkHeartbeats);
|
||||
this->heartbeatTimer.setInterval(this->heartbeatInterval);
|
||||
this->heartbeatTimer.setSingleShot(false);
|
||||
this->heartbeatTimer.start();
|
||||
}
|
||||
|
||||
SeventvEventAPI::~SeventvEventAPI()
|
||||
SeventvEventAPIPrivate::~SeventvEventAPIPrivate()
|
||||
{
|
||||
this->stop();
|
||||
}
|
||||
|
||||
std::shared_ptr<Client> SeventvEventAPIPrivate::makeClient()
|
||||
{
|
||||
return std::make_shared<Client>(this->parent, this->heartbeatInterval);
|
||||
}
|
||||
|
||||
void SeventvEventAPIPrivate::checkHeartbeats()
|
||||
{
|
||||
auto minInterval = std::chrono::milliseconds::max();
|
||||
for (const auto &[id, client] : this->clients())
|
||||
{
|
||||
client->checkHeartbeat();
|
||||
minInterval = std::min(minInterval, client->heartbeatInterval());
|
||||
}
|
||||
if (minInterval != std::chrono::milliseconds::max())
|
||||
{
|
||||
this->heartbeatInterval = minInterval;
|
||||
this->heartbeatTimer.setInterval(this->heartbeatInterval);
|
||||
}
|
||||
}
|
||||
|
||||
SeventvEventAPI::SeventvEventAPI(
|
||||
QString host, std::chrono::milliseconds defaultHeartbeatInterval)
|
||||
: private_(std::make_unique<SeventvEventAPIPrivate>(
|
||||
*this, std::move(host), defaultHeartbeatInterval))
|
||||
{
|
||||
}
|
||||
|
||||
SeventvEventAPI::~SeventvEventAPI() = default;
|
||||
|
||||
void SeventvEventAPI::subscribeUser(const QString &userID,
|
||||
const QString &emoteSetID)
|
||||
{
|
||||
if (!userID.isEmpty() && this->subscribedUsers_.insert(userID).second)
|
||||
if (!userID.isEmpty() &&
|
||||
this->private_->subscribedUsers.insert(userID).second)
|
||||
{
|
||||
this->subscribe(
|
||||
this->private_->subscribe(
|
||||
{ObjectIDCondition{userID}, SubscriptionType::UpdateUser});
|
||||
}
|
||||
if (!emoteSetID.isEmpty() &&
|
||||
this->subscribedEmoteSets_.insert(emoteSetID).second)
|
||||
this->private_->subscribedEmoteSets.insert(emoteSetID).second)
|
||||
{
|
||||
this->subscribe(
|
||||
this->private_->subscribe(
|
||||
{ObjectIDCondition{emoteSetID}, SubscriptionType::UpdateEmoteSet});
|
||||
}
|
||||
}
|
||||
|
||||
void SeventvEventAPI::subscribeTwitchChannel(const QString &id)
|
||||
{
|
||||
if (this->subscribedTwitchChannels_.insert(id).second)
|
||||
if (this->private_->subscribedTwitchChannels.insert(id).second)
|
||||
{
|
||||
this->subscribe({
|
||||
this->private_->subscribe({
|
||||
ChannelCondition{id},
|
||||
SubscriptionType::CreateCosmetic,
|
||||
});
|
||||
this->subscribe({
|
||||
this->private_->subscribe({
|
||||
ChannelCondition{id},
|
||||
SubscriptionType::CreateEntitlement,
|
||||
});
|
||||
this->subscribe({
|
||||
this->private_->subscribe({
|
||||
ChannelCondition{id},
|
||||
SubscriptionType::DeleteEntitlement,
|
||||
});
|
||||
@@ -68,362 +132,49 @@ void SeventvEventAPI::subscribeTwitchChannel(const QString &id)
|
||||
|
||||
void SeventvEventAPI::unsubscribeEmoteSet(const QString &id)
|
||||
{
|
||||
if (this->subscribedEmoteSets_.erase(id) > 0)
|
||||
if (this->private_->subscribedEmoteSets.erase(id) > 0)
|
||||
{
|
||||
this->unsubscribe(
|
||||
this->private_->unsubscribe(
|
||||
{ObjectIDCondition{id}, SubscriptionType::UpdateEmoteSet});
|
||||
}
|
||||
}
|
||||
|
||||
void SeventvEventAPI::unsubscribeUser(const QString &id)
|
||||
{
|
||||
if (this->subscribedUsers_.erase(id) > 0)
|
||||
if (this->private_->subscribedUsers.erase(id) > 0)
|
||||
{
|
||||
this->unsubscribe(
|
||||
this->private_->unsubscribe(
|
||||
{ObjectIDCondition{id}, SubscriptionType::UpdateUser});
|
||||
}
|
||||
}
|
||||
|
||||
void SeventvEventAPI::unsubscribeTwitchChannel(const QString &id)
|
||||
{
|
||||
if (this->subscribedTwitchChannels_.erase(id) > 0)
|
||||
if (this->private_->subscribedTwitchChannels.erase(id) > 0)
|
||||
{
|
||||
this->unsubscribe({
|
||||
this->private_->unsubscribe({
|
||||
ChannelCondition{id},
|
||||
SubscriptionType::CreateCosmetic,
|
||||
});
|
||||
this->unsubscribe({
|
||||
this->private_->unsubscribe({
|
||||
ChannelCondition{id},
|
||||
SubscriptionType::CreateEntitlement,
|
||||
});
|
||||
this->unsubscribe({
|
||||
this->private_->unsubscribe({
|
||||
ChannelCondition{id},
|
||||
SubscriptionType::DeleteEntitlement,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<BasicPubSubClient<Subscription>> SeventvEventAPI::createClient(
|
||||
liveupdates::WebsocketClient &client, websocketpp::connection_hdl hdl)
|
||||
void SeventvEventAPI::stop()
|
||||
{
|
||||
auto shared =
|
||||
std::make_shared<Client>(client, hdl, this->heartbeatInterval_);
|
||||
return std::static_pointer_cast<BasicPubSubClient<Subscription>>(
|
||||
std::move(shared));
|
||||
this->private_->stop();
|
||||
}
|
||||
|
||||
void SeventvEventAPI::onMessage(
|
||||
websocketpp::connection_hdl hdl,
|
||||
BasicPubSubManager<Subscription>::WebsocketMessagePtr msg)
|
||||
const liveupdates::Diag &SeventvEventAPI::diag() const
|
||||
{
|
||||
const auto &payload = QString::fromStdString(msg->get_payload());
|
||||
|
||||
auto pMessage = parseBaseMessage(payload);
|
||||
|
||||
if (!pMessage)
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Unable to parse incoming event-api message: " << payload;
|
||||
return;
|
||||
}
|
||||
auto message = *pMessage;
|
||||
switch (message.op)
|
||||
{
|
||||
case Opcode::Hello: {
|
||||
if (auto client = this->findClient(hdl))
|
||||
{
|
||||
if (auto *stvClient = dynamic_cast<Client *>(client.get()))
|
||||
{
|
||||
stvClient->setHeartbeatInterval(
|
||||
message.data["heartbeat_interval"].toInt());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Opcode::Heartbeat: {
|
||||
if (auto client = this->findClient(hdl))
|
||||
{
|
||||
if (auto *stvClient = dynamic_cast<Client *>(client.get()))
|
||||
{
|
||||
stvClient->handleHeartbeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Opcode::Dispatch: {
|
||||
auto dispatch = message.toInner<Dispatch>();
|
||||
if (!dispatch)
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Malformed dispatch" << payload;
|
||||
return;
|
||||
}
|
||||
this->handleDispatch(*dispatch);
|
||||
}
|
||||
break;
|
||||
case Opcode::Reconnect: {
|
||||
if (auto client = this->findClient(hdl))
|
||||
{
|
||||
if (auto *stvClient = dynamic_cast<Client *>(client.get()))
|
||||
{
|
||||
stvClient->close("Reconnecting");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Opcode::Ack: {
|
||||
// unhandled
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
qCDebug(chatterinoSeventvEventAPI) << "Unhandled op:" << payload;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return this->private_->diag;
|
||||
}
|
||||
|
||||
void SeventvEventAPI::handleDispatch(const Dispatch &dispatch)
|
||||
{
|
||||
switch (dispatch.type)
|
||||
{
|
||||
case SubscriptionType::UpdateEmoteSet: {
|
||||
this->onEmoteSetUpdate(dispatch);
|
||||
}
|
||||
break;
|
||||
case SubscriptionType::UpdateUser: {
|
||||
this->onUserUpdate(dispatch);
|
||||
}
|
||||
break;
|
||||
case SubscriptionType::CreateCosmetic: {
|
||||
const CosmeticCreateDispatch cosmetic(dispatch);
|
||||
if (cosmetic.validate())
|
||||
{
|
||||
this->onCosmeticCreate(cosmetic);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid cosmetic dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SubscriptionType::CreateEntitlement: {
|
||||
const EntitlementCreateDeleteDispatch entitlement(dispatch);
|
||||
if (entitlement.validate())
|
||||
{
|
||||
this->onEntitlementCreate(entitlement);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid entitlement create dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SubscriptionType::DeleteEntitlement: {
|
||||
const EntitlementCreateDeleteDispatch entitlement(dispatch);
|
||||
if (entitlement.validate())
|
||||
{
|
||||
this->onEntitlementDelete(entitlement);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid entitlement delete dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SubscriptionType::ResetEntitlement: {
|
||||
// unhandled (not clear what we'd do here yet)
|
||||
}
|
||||
break;
|
||||
case SubscriptionType::CreateEmoteSet: {
|
||||
// unhandled (c2 does not support custom emote sets)
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Unknown subscription type:"
|
||||
<< qmagicenum::enumName(dispatch.type)
|
||||
<< "body:" << dispatch.body;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SeventvEventAPI::onEmoteSetUpdate(const Dispatch &dispatch)
|
||||
{
|
||||
// dispatchBody: {
|
||||
// pushed: Array<{ key, value }>,
|
||||
// pulled: Array<{ key, old_value }>,
|
||||
// updated: Array<{ key, value, old_value }>,
|
||||
// }
|
||||
for (const auto pushedRef : dispatch.body["pushed"].toArray())
|
||||
{
|
||||
auto pushed = pushedRef.toObject();
|
||||
if (pushed["key"].toString() != "emotes")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const EmoteAddDispatch added(dispatch, pushed["value"].toObject());
|
||||
|
||||
if (added.validate())
|
||||
{
|
||||
this->signals_.emoteAdded.invoke(added);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
for (const auto updatedRef : dispatch.body["updated"].toArray())
|
||||
{
|
||||
auto updated = updatedRef.toObject();
|
||||
if (updated["key"].toString() != "emotes")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const EmoteUpdateDispatch update(dispatch,
|
||||
updated["old_value"].toObject(),
|
||||
updated["value"].toObject());
|
||||
|
||||
if (update.validate())
|
||||
{
|
||||
this->signals_.emoteUpdated.invoke(update);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
for (const auto pulledRef : dispatch.body["pulled"].toArray())
|
||||
{
|
||||
auto pulled = pulledRef.toObject();
|
||||
if (pulled["key"].toString() != "emotes")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const EmoteRemoveDispatch removed(dispatch,
|
||||
pulled["old_value"].toObject());
|
||||
|
||||
if (removed.validate())
|
||||
{
|
||||
this->signals_.emoteRemoved.invoke(removed);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SeventvEventAPI::onUserUpdate(const Dispatch &dispatch)
|
||||
{
|
||||
// dispatchBody: {
|
||||
// updated: Array<{ key, value: Array<{key, value}> }>
|
||||
// }
|
||||
for (const auto updatedRef : dispatch.body["updated"].toArray())
|
||||
{
|
||||
auto updated = updatedRef.toObject();
|
||||
if (updated["key"].toString() != "connections")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const auto valueRef : updated["value"].toArray())
|
||||
{
|
||||
auto value = valueRef.toObject();
|
||||
if (value["key"].toString() != "emote_set")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const UserConnectionUpdateDispatch update(
|
||||
dispatch, value, (size_t)updated["index"].toInt());
|
||||
|
||||
if (update.validate())
|
||||
{
|
||||
this->signals_.userUpdated.invoke(update);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(readability-convert-member-functions-to-static)
|
||||
|
||||
void SeventvEventAPI::onCosmeticCreate(const CosmeticCreateDispatch &cosmetic)
|
||||
{
|
||||
auto *app = tryGetApp();
|
||||
if (!app)
|
||||
{
|
||||
return; // shutting down
|
||||
}
|
||||
|
||||
auto *badges = app->getSeventvBadges();
|
||||
switch (cosmetic.kind)
|
||||
{
|
||||
case CosmeticKind::Badge: {
|
||||
badges->registerBadge(cosmetic.data);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SeventvEventAPI::onEntitlementCreate(
|
||||
const EntitlementCreateDeleteDispatch &entitlement)
|
||||
{
|
||||
auto *app = tryGetApp();
|
||||
if (!app)
|
||||
{
|
||||
return; // shutting down
|
||||
}
|
||||
|
||||
auto *badges = app->getSeventvBadges();
|
||||
switch (entitlement.kind)
|
||||
{
|
||||
case CosmeticKind::Badge: {
|
||||
badges->assignBadgeToUser(entitlement.refID,
|
||||
UserId{entitlement.userID});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SeventvEventAPI::onEntitlementDelete(
|
||||
const EntitlementCreateDeleteDispatch &entitlement)
|
||||
{
|
||||
auto *app = tryGetApp();
|
||||
if (!app)
|
||||
{
|
||||
return; // shutting down
|
||||
}
|
||||
|
||||
auto *badges = app->getSeventvBadges();
|
||||
switch (entitlement.kind)
|
||||
{
|
||||
case CosmeticKind::Badge: {
|
||||
badges->clearBadgeFromUser(entitlement.refID,
|
||||
UserId{entitlement.userID});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
// NOLINTEND(readability-convert-member-functions-to-static)
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,39 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "providers/liveupdates/BasicPubSubClient.hpp"
|
||||
#include "providers/liveupdates/BasicPubSubManager.hpp"
|
||||
#include "providers/seventv/eventapi/Subscription.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
#include <QString>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace liveupdates {
|
||||
struct Diag;
|
||||
} // namespace liveupdates
|
||||
|
||||
namespace seventv::eventapi {
|
||||
struct Dispatch;
|
||||
struct EmoteAddDispatch;
|
||||
struct EmoteUpdateDispatch;
|
||||
struct EmoteRemoveDispatch;
|
||||
struct UserConnectionUpdateDispatch;
|
||||
struct CosmeticCreateDispatch;
|
||||
struct EntitlementCreateDeleteDispatch;
|
||||
} // namespace seventv::eventapi
|
||||
|
||||
class SeventvBadges;
|
||||
|
||||
class SeventvEventAPIPrivate;
|
||||
class SeventvEventAPI
|
||||
: public BasicPubSubManager<seventv::eventapi::Subscription>
|
||||
{
|
||||
template <typename T>
|
||||
using Signal =
|
||||
pajlada::Signals::Signal<T>; // type-id is vector<T, Alloc<T>>
|
||||
using Signal = pajlada::Signals::Signal<T>;
|
||||
|
||||
public:
|
||||
SeventvEventAPI(QString host,
|
||||
std::chrono::milliseconds defaultHeartbeatInterval =
|
||||
std::chrono::milliseconds(25000));
|
||||
|
||||
~SeventvEventAPI() override;
|
||||
~SeventvEventAPI();
|
||||
|
||||
struct {
|
||||
Signal<seventv::eventapi::EmoteAddDispatch> emoteAdded;
|
||||
@@ -65,34 +62,19 @@ public:
|
||||
/** Unsubscribes from cosmetics and entitlements in a Twitch channel */
|
||||
void unsubscribeTwitchChannel(const QString &id);
|
||||
|
||||
protected:
|
||||
std::shared_ptr<BasicPubSubClient<seventv::eventapi::Subscription>>
|
||||
createClient(liveupdates::WebsocketClient &client,
|
||||
websocketpp::connection_hdl hdl) override;
|
||||
void onMessage(
|
||||
websocketpp::connection_hdl hdl,
|
||||
BasicPubSubManager<seventv::eventapi::Subscription>::WebsocketMessagePtr
|
||||
msg) override;
|
||||
/// Stop the manager
|
||||
///
|
||||
/// Used in tests to check that connections are closed (through #diag()).
|
||||
/// Otherwise, calling the destructor is sufficient.
|
||||
void stop();
|
||||
|
||||
/// Statistics about the opened/closed connections and received messages
|
||||
///
|
||||
/// Used in tests.
|
||||
const liveupdates::Diag &diag() const;
|
||||
|
||||
private:
|
||||
void handleDispatch(const seventv::eventapi::Dispatch &dispatch);
|
||||
|
||||
void onEmoteSetUpdate(const seventv::eventapi::Dispatch &dispatch);
|
||||
void onUserUpdate(const seventv::eventapi::Dispatch &dispatch);
|
||||
void onCosmeticCreate(
|
||||
const seventv::eventapi::CosmeticCreateDispatch &cosmetic);
|
||||
void onEntitlementCreate(
|
||||
const seventv::eventapi::EntitlementCreateDeleteDispatch &entitlement);
|
||||
void onEntitlementDelete(
|
||||
const seventv::eventapi::EntitlementCreateDeleteDispatch &entitlement);
|
||||
|
||||
/** emote-set ids */
|
||||
std::unordered_set<QString> subscribedEmoteSets_;
|
||||
/** user ids */
|
||||
std::unordered_set<QString> subscribedUsers_;
|
||||
/** Twitch channel ids */
|
||||
std::unordered_set<QString> subscribedTwitchChannels_;
|
||||
std::chrono::milliseconds heartbeatInterval_;
|
||||
std::unique_ptr<SeventvEventAPIPrivate> private_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,73 +1,343 @@
|
||||
#include "providers/seventv/eventapi/Client.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "providers/seventv/eventapi/Dispatch.hpp"
|
||||
#include "providers/seventv/eventapi/Message.hpp"
|
||||
#include "providers/seventv/eventapi/Subscription.hpp"
|
||||
#include "providers/twitch/PubSubHelpers.hpp"
|
||||
#include "providers/seventv/SeventvBadges.hpp"
|
||||
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||
#include "util/QMagicEnum.hpp"
|
||||
|
||||
#include <utility>
|
||||
#include <QJsonArray>
|
||||
|
||||
namespace chatterino::seventv::eventapi {
|
||||
|
||||
Client::Client(liveupdates::WebsocketClient &websocketClient,
|
||||
liveupdates::WebsocketHandle handle,
|
||||
Client::Client(SeventvEventAPI &manager,
|
||||
std::chrono::milliseconds heartbeatInterval)
|
||||
: BasicPubSubClient<Subscription>(websocketClient, std::move(handle))
|
||||
: BasicPubSubClient<Subscription>(100)
|
||||
, lastHeartbeat_(std::chrono::steady_clock::now())
|
||||
, heartbeatInterval_(heartbeatInterval)
|
||||
, heartbeatTimer_(std::make_shared<boost::asio::steady_timer>(
|
||||
this->websocketClient_.get_io_service()))
|
||||
, manager_(manager)
|
||||
{
|
||||
}
|
||||
|
||||
void Client::stopImpl()
|
||||
{
|
||||
this->heartbeatTimer_->cancel();
|
||||
}
|
||||
|
||||
void Client::onConnectionEstablished()
|
||||
void Client::onOpen()
|
||||
{
|
||||
BasicPubSubClient<Subscription>::onOpen();
|
||||
this->lastHeartbeat_.store(std::chrono::steady_clock::now(),
|
||||
std::memory_order_release);
|
||||
this->checkHeartbeat();
|
||||
std::memory_order::relaxed);
|
||||
}
|
||||
|
||||
void Client::setHeartbeatInterval(int intervalMs)
|
||||
void Client::onMessage(const QByteArray &msg)
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Setting expected heartbeat interval to" << intervalMs << "ms";
|
||||
this->heartbeatInterval_ = std::chrono::milliseconds(intervalMs);
|
||||
}
|
||||
auto pMessage = parseBaseMessage(msg);
|
||||
|
||||
void Client::handleHeartbeat()
|
||||
{
|
||||
this->lastHeartbeat_.store(std::chrono::steady_clock::now(),
|
||||
std::memory_order_release);
|
||||
if (!pMessage)
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Unable to parse incoming event-api message: " << msg;
|
||||
return;
|
||||
}
|
||||
auto message = *pMessage;
|
||||
switch (message.op)
|
||||
{
|
||||
case Opcode::Hello: {
|
||||
this->heartbeatInterval_.store(
|
||||
std::chrono::milliseconds{
|
||||
message.data["heartbeat_interval"].toInt()},
|
||||
std::memory_order::relaxed);
|
||||
}
|
||||
break;
|
||||
case Opcode::Heartbeat: {
|
||||
this->lastHeartbeat_.store(std::chrono::steady_clock::now(),
|
||||
std::memory_order::relaxed);
|
||||
}
|
||||
break;
|
||||
case Opcode::Dispatch: {
|
||||
auto dispatch = message.toInner<Dispatch>();
|
||||
if (!dispatch)
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Malformed dispatch" << msg;
|
||||
return;
|
||||
}
|
||||
this->handleDispatch(*dispatch);
|
||||
}
|
||||
break;
|
||||
case Opcode::Reconnect: {
|
||||
this->close();
|
||||
}
|
||||
break;
|
||||
case Opcode::Ack: {
|
||||
// unhandled
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
qCDebug(chatterinoSeventvEventAPI) << "Unhandled op:" << msg;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Client::checkHeartbeat()
|
||||
{
|
||||
// Following the heartbeat docs, a connection is dead
|
||||
// after three missed heartbeats.
|
||||
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#heartbeat
|
||||
assert(this->isStarted());
|
||||
if ((std::chrono::steady_clock::now() - this->lastHeartbeat_.load()) >
|
||||
3 * this->heartbeatInterval_)
|
||||
if ((std::chrono::steady_clock::now() -
|
||||
this->lastHeartbeat_.load(std::memory_order::relaxed)) >
|
||||
this->heartbeatInterval() * 3)
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Didn't receive a heartbeat in time, disconnecting!";
|
||||
this->close("Didn't receive a heartbeat in time");
|
||||
|
||||
return;
|
||||
qCDebug(chatterinoSeventvEventAPI) << "Hearbeat timed out";
|
||||
this->close();
|
||||
}
|
||||
|
||||
auto self = std::dynamic_pointer_cast<Client>(this->shared_from_this());
|
||||
|
||||
runAfter(this->heartbeatTimer_, this->heartbeatInterval_, [self](auto) {
|
||||
if (!self->isStarted())
|
||||
{
|
||||
return;
|
||||
}
|
||||
self->checkHeartbeat();
|
||||
});
|
||||
}
|
||||
|
||||
std::chrono::milliseconds Client::heartbeatInterval() const
|
||||
{
|
||||
return this->heartbeatInterval_.load(std::memory_order::relaxed);
|
||||
}
|
||||
|
||||
void Client::handleDispatch(const Dispatch &dispatch)
|
||||
{
|
||||
switch (dispatch.type)
|
||||
{
|
||||
case SubscriptionType::UpdateEmoteSet: {
|
||||
this->onEmoteSetUpdate(dispatch);
|
||||
}
|
||||
break;
|
||||
case SubscriptionType::UpdateUser: {
|
||||
this->onUserUpdate(dispatch);
|
||||
}
|
||||
break;
|
||||
case SubscriptionType::CreateCosmetic: {
|
||||
const CosmeticCreateDispatch cosmetic(dispatch);
|
||||
if (cosmetic.validate())
|
||||
{
|
||||
this->onCosmeticCreate(cosmetic);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid cosmetic dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SubscriptionType::CreateEntitlement: {
|
||||
const EntitlementCreateDeleteDispatch entitlement(dispatch);
|
||||
if (entitlement.validate())
|
||||
{
|
||||
this->onEntitlementCreate(entitlement);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid entitlement create dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SubscriptionType::DeleteEntitlement: {
|
||||
const EntitlementCreateDeleteDispatch entitlement(dispatch);
|
||||
if (entitlement.validate())
|
||||
{
|
||||
this->onEntitlementDelete(entitlement);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid entitlement delete dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
break;
|
||||
// NOLINTNEXTLINE(bugprone-branch-clone)
|
||||
case SubscriptionType::ResetEntitlement: {
|
||||
// unhandled (not clear what we'd do here yet)
|
||||
}
|
||||
break;
|
||||
case SubscriptionType::CreateEmoteSet: {
|
||||
// unhandled (c2 does not support custom emote sets)
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Unknown subscription type:"
|
||||
<< qmagicenum::enumName(dispatch.type)
|
||||
<< "body:" << dispatch.body;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Client::onEmoteSetUpdate(const Dispatch &dispatch)
|
||||
{
|
||||
// dispatchBody: {
|
||||
// pushed: Array<{ key, value }>,
|
||||
// pulled: Array<{ key, old_value }>,
|
||||
// updated: Array<{ key, value, old_value }>,
|
||||
// }
|
||||
for (const auto pushedRef : dispatch.body["pushed"].toArray())
|
||||
{
|
||||
auto pushed = pushedRef.toObject();
|
||||
if (pushed["key"].toString() != "emotes")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const EmoteAddDispatch added(dispatch, pushed["value"].toObject());
|
||||
|
||||
if (added.validate())
|
||||
{
|
||||
this->manager_.signals_.emoteAdded.invoke(added);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
for (const auto updatedRef : dispatch.body["updated"].toArray())
|
||||
{
|
||||
auto updated = updatedRef.toObject();
|
||||
if (updated["key"].toString() != "emotes")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const EmoteUpdateDispatch update(dispatch,
|
||||
updated["old_value"].toObject(),
|
||||
updated["value"].toObject());
|
||||
|
||||
if (update.validate())
|
||||
{
|
||||
this->manager_.signals_.emoteUpdated.invoke(update);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
for (const auto pulledRef : dispatch.body["pulled"].toArray())
|
||||
{
|
||||
auto pulled = pulledRef.toObject();
|
||||
if (pulled["key"].toString() != "emotes")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const EmoteRemoveDispatch removed(dispatch,
|
||||
pulled["old_value"].toObject());
|
||||
|
||||
if (removed.validate())
|
||||
{
|
||||
this->manager_.signals_.emoteRemoved.invoke(removed);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Client::onUserUpdate(const Dispatch &dispatch)
|
||||
{
|
||||
// dispatchBody: {
|
||||
// updated: Array<{ key, value: Array<{key, value}> }>
|
||||
// }
|
||||
for (const auto updatedRef : dispatch.body["updated"].toArray())
|
||||
{
|
||||
auto updated = updatedRef.toObject();
|
||||
if (updated["key"].toString() != "connections")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const auto valueRef : updated["value"].toArray())
|
||||
{
|
||||
auto value = valueRef.toObject();
|
||||
if (value["key"].toString() != "emote_set")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const UserConnectionUpdateDispatch update(
|
||||
dispatch, value, (size_t)updated["index"].toInt());
|
||||
|
||||
if (update.validate())
|
||||
{
|
||||
this->manager_.signals_.userUpdated.invoke(update);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoSeventvEventAPI)
|
||||
<< "Invalid dispatch" << dispatch.body;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(readability-convert-member-functions-to-static)
|
||||
|
||||
void Client::onCosmeticCreate(const CosmeticCreateDispatch &cosmetic)
|
||||
{
|
||||
auto *app = tryGetApp();
|
||||
if (!app)
|
||||
{
|
||||
return; // shutting down
|
||||
}
|
||||
|
||||
auto *badges = app->getSeventvBadges();
|
||||
switch (cosmetic.kind)
|
||||
{
|
||||
case CosmeticKind::Badge: {
|
||||
badges->registerBadge(cosmetic.data);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Client::onEntitlementCreate(
|
||||
const EntitlementCreateDeleteDispatch &entitlement)
|
||||
{
|
||||
auto *app = tryGetApp();
|
||||
if (!app)
|
||||
{
|
||||
return; // shutting down
|
||||
}
|
||||
|
||||
auto *badges = app->getSeventvBadges();
|
||||
switch (entitlement.kind)
|
||||
{
|
||||
case CosmeticKind::Badge: {
|
||||
badges->assignBadgeToUser(entitlement.refID,
|
||||
UserId{entitlement.userID});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Client::onEntitlementDelete(
|
||||
const EntitlementCreateDeleteDispatch &entitlement)
|
||||
{
|
||||
auto *app = tryGetApp();
|
||||
if (!app)
|
||||
{
|
||||
return; // shutting down
|
||||
}
|
||||
|
||||
auto *badges = app->getSeventvBadges();
|
||||
switch (entitlement.kind)
|
||||
{
|
||||
case CosmeticKind::Badge: {
|
||||
badges->clearBadgeFromUser(entitlement.refID,
|
||||
UserId{entitlement.userID});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
// NOLINTEND(readability-convert-member-functions-to-static)
|
||||
|
||||
} // namespace chatterino::seventv::eventapi
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// of std::hash for Subscription
|
||||
#include "providers/seventv/eventapi/Subscription.hpp"
|
||||
|
||||
#include <QPointer>
|
||||
|
||||
namespace chatterino {
|
||||
class SeventvEventAPI;
|
||||
|
||||
@@ -12,31 +14,38 @@ class SeventvEventAPI;
|
||||
|
||||
namespace chatterino::seventv::eventapi {
|
||||
|
||||
class Client : public BasicPubSubClient<Subscription>
|
||||
struct Dispatch;
|
||||
struct CosmeticCreateDispatch;
|
||||
struct EntitlementCreateDeleteDispatch;
|
||||
|
||||
class Client : public BasicPubSubClient<Subscription>,
|
||||
std::enable_shared_from_this<Client>
|
||||
{
|
||||
public:
|
||||
Client(liveupdates::WebsocketClient &websocketClient,
|
||||
liveupdates::WebsocketHandle handle,
|
||||
Client(SeventvEventAPI &manager,
|
||||
std::chrono::milliseconds heartbeatInterval);
|
||||
|
||||
void stopImpl() override;
|
||||
void onOpen() /* override */;
|
||||
void onMessage(const QByteArray &msg) /* override */;
|
||||
|
||||
void setHeartbeatInterval(int intervalMs);
|
||||
void handleHeartbeat();
|
||||
|
||||
protected:
|
||||
void onConnectionEstablished() override;
|
||||
std::chrono::milliseconds heartbeatInterval() const;
|
||||
void checkHeartbeat();
|
||||
|
||||
private:
|
||||
void checkHeartbeat();
|
||||
void handleDispatch(const Dispatch &dispatch);
|
||||
|
||||
void onEmoteSetUpdate(const Dispatch &dispatch);
|
||||
void onUserUpdate(const Dispatch &dispatch);
|
||||
void onCosmeticCreate(const CosmeticCreateDispatch &cosmetic);
|
||||
void onEntitlementCreate(
|
||||
const EntitlementCreateDeleteDispatch &entitlement);
|
||||
void onEntitlementDelete(
|
||||
const EntitlementCreateDeleteDispatch &entitlement);
|
||||
|
||||
std::atomic<std::chrono::time_point<std::chrono::steady_clock>>
|
||||
lastHeartbeat_;
|
||||
// This will be set once on the welcome message.
|
||||
std::chrono::milliseconds heartbeatInterval_;
|
||||
std::shared_ptr<boost::asio::steady_timer> heartbeatTimer_;
|
||||
|
||||
friend SeventvEventAPI;
|
||||
std::atomic<std::chrono::milliseconds> heartbeatInterval_;
|
||||
SeventvEventAPI &manager_;
|
||||
};
|
||||
|
||||
} // namespace chatterino::seventv::eventapi
|
||||
|
||||
@@ -8,9 +8,9 @@ Message::Message(QJsonObject _json)
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<Message> parseBaseMessage(const QString &blob)
|
||||
std::optional<Message> parseBaseMessage(const QByteArray &blob)
|
||||
{
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8()));
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(blob));
|
||||
|
||||
if (jsonDoc.isNull())
|
||||
{
|
||||
|
||||
@@ -28,6 +28,6 @@ std::optional<InnerClass> Message::toInner()
|
||||
return InnerClass{this->data};
|
||||
}
|
||||
|
||||
std::optional<Message> parseBaseMessage(const QString &blob);
|
||||
std::optional<Message> parseBaseMessage(const QByteArray &blob);
|
||||
|
||||
} // namespace chatterino::seventv::eventapi
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" // IWYU pragma: keep
|
||||
#include "providers/ffz/FfzEmotes.hpp"
|
||||
#include "providers/irc/IrcConnection2.hpp"
|
||||
#include "providers/seventv/eventapi/Dispatch.hpp" // IWYU pragma: keep
|
||||
@@ -880,8 +881,6 @@ void TwitchIrcServer::initEventAPIs(BttvLiveUpdates *bttvLiveUpdates,
|
||||
},
|
||||
this);
|
||||
});
|
||||
|
||||
bttvLiveUpdates->start();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -932,8 +931,6 @@ void TwitchIrcServer::initEventAPIs(BttvLiveUpdates *bttvLiveUpdates,
|
||||
chan.updateSeventvUser(data);
|
||||
});
|
||||
});
|
||||
|
||||
seventvEventAPI->start();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+43
-16
@@ -1,3 +1,4 @@
|
||||
#include "mocks/BaseApplication.hpp"
|
||||
#include "providers/liveupdates/BasicPubSubClient.hpp"
|
||||
#include "providers/liveupdates/BasicPubSubManager.hpp"
|
||||
#include "Test.hpp"
|
||||
@@ -6,6 +7,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QtCore/qtestsupport_core.h>
|
||||
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
@@ -14,6 +16,8 @@
|
||||
using namespace chatterino;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
|
||||
struct DummySubscription {
|
||||
int type;
|
||||
QString condition;
|
||||
@@ -54,6 +58,8 @@ struct DummySubscription {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace std {
|
||||
template <>
|
||||
struct hash<DummySubscription> {
|
||||
@@ -64,7 +70,24 @@ struct hash<DummySubscription> {
|
||||
};
|
||||
} // namespace std
|
||||
|
||||
class MyManager : public BasicPubSubManager<DummySubscription>
|
||||
namespace {
|
||||
|
||||
class MyManager;
|
||||
class MyClient : public BasicPubSubClient<DummySubscription>
|
||||
{
|
||||
public:
|
||||
MyClient(MyManager &manager)
|
||||
: manager(manager)
|
||||
{
|
||||
}
|
||||
|
||||
void onMessage(const QByteArray &msg) /* override */;
|
||||
|
||||
private:
|
||||
MyManager &manager;
|
||||
};
|
||||
|
||||
class MyManager : public BasicPubSubManager<MyManager, MyClient>
|
||||
{
|
||||
public:
|
||||
MyManager(QString host)
|
||||
@@ -97,33 +120,34 @@ public:
|
||||
this->unsubscribe(sub);
|
||||
}
|
||||
|
||||
protected:
|
||||
void onMessage(
|
||||
websocketpp::connection_hdl /*hdl*/,
|
||||
BasicPubSubManager<DummySubscription>::WebsocketMessagePtr msg) override
|
||||
std::shared_ptr<MyClient> makeClient()
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(this->messageMtx_);
|
||||
this->messagesReceived.fetch_add(1, std::memory_order_acq_rel);
|
||||
this->messageQueue_.emplace_back(
|
||||
QString::fromStdString(msg->get_payload()));
|
||||
return std::make_shared<MyClient>(*this);
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex messageMtx_;
|
||||
std::deque<QString> messageQueue_;
|
||||
|
||||
friend MyClient;
|
||||
};
|
||||
|
||||
void MyClient::onMessage(const QByteArray &msg)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(this->manager.messageMtx_);
|
||||
this->manager.messagesReceived.fetch_add(1, std::memory_order_acq_rel);
|
||||
this->manager.messageQueue_.emplace_back(QString::fromUtf8(msg));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(BasicPubSub, SubscriptionCycle)
|
||||
{
|
||||
mock::BaseApplication app;
|
||||
const QString host("wss://127.0.0.1:9050/liveupdates/sub-unsub");
|
||||
MyManager manager(host);
|
||||
manager.start();
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
manager.sub({1, "foo"});
|
||||
std::this_thread::sleep_for(500ms);
|
||||
|
||||
ASSERT_EQ(manager.diag.connectionsOpened, 1);
|
||||
QTest::qWait(500);
|
||||
ASSERT_EQ(manager.diag.connectionsClosed, 0);
|
||||
ASSERT_EQ(manager.diag.connectionsFailed, 0);
|
||||
ASSERT_EQ(manager.messagesReceived, 1);
|
||||
@@ -131,7 +155,7 @@ TEST(BasicPubSub, SubscriptionCycle)
|
||||
ASSERT_EQ(manager.popMessage(), QString("ack-sub-1-foo"));
|
||||
|
||||
manager.unsub({1, "foo"});
|
||||
std::this_thread::sleep_for(50ms);
|
||||
QTest::qWait(50);
|
||||
|
||||
ASSERT_EQ(manager.diag.connectionsOpened, 1);
|
||||
ASSERT_EQ(manager.diag.connectionsClosed, 0);
|
||||
@@ -140,6 +164,9 @@ TEST(BasicPubSub, SubscriptionCycle)
|
||||
ASSERT_EQ(manager.popMessage(), QString("ack-unsub-1-foo"));
|
||||
|
||||
manager.stop();
|
||||
// after exactly one event loop iteration, we should see updated counters
|
||||
QCoreApplication::processEvents(QEventLoop::AllEvents);
|
||||
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
|
||||
|
||||
ASSERT_EQ(manager.diag.connectionsOpened, 1);
|
||||
ASSERT_EQ(manager.diag.connectionsClosed, 1);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||
|
||||
#include "mocks/BaseApplication.hpp"
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||
#include "providers/liveupdates/Diag.hpp"
|
||||
#include "Test.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QtCore/qtestsupport_core.h>
|
||||
|
||||
#include <optional>
|
||||
#include <tuple>
|
||||
@@ -15,9 +19,10 @@ const QString TARGET_USER_NAME = "Alien";
|
||||
|
||||
TEST(BttvLiveUpdates, AllEvents)
|
||||
{
|
||||
mock::BaseApplication app;
|
||||
|
||||
const QString host("wss://127.0.0.1:9050/liveupdates/bttv/all-events");
|
||||
chatterino::BttvLiveUpdates liveUpdates(host);
|
||||
liveUpdates.start();
|
||||
|
||||
std::optional<BttvLiveUpdateEmoteUpdateAddMessage> addMessage;
|
||||
std::optional<BttvLiveUpdateEmoteUpdateAddMessage> updateMessage;
|
||||
@@ -33,13 +38,12 @@ TEST(BttvLiveUpdates, AllEvents)
|
||||
removeMessage = m;
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
liveUpdates.joinChannel(TARGET_USER_ID, TARGET_USER_NAME);
|
||||
std::this_thread::sleep_for(500ms);
|
||||
QTest::qWait(500);
|
||||
|
||||
ASSERT_EQ(liveUpdates.diag.connectionsOpened, 1);
|
||||
ASSERT_EQ(liveUpdates.diag.connectionsClosed, 0);
|
||||
ASSERT_EQ(liveUpdates.diag.connectionsFailed, 0);
|
||||
ASSERT_EQ(liveUpdates.diag().connectionsOpened, 1);
|
||||
ASSERT_EQ(liveUpdates.diag().connectionsClosed, 0);
|
||||
ASSERT_EQ(liveUpdates.diag().connectionsFailed, 0);
|
||||
|
||||
auto add = *addMessage;
|
||||
ASSERT_EQ(add.channelID, TARGET_USER_ID);
|
||||
@@ -56,7 +60,11 @@ TEST(BttvLiveUpdates, AllEvents)
|
||||
ASSERT_EQ(rem.emoteID, QString("55898e122612142e6aaa935b"));
|
||||
|
||||
liveUpdates.stop();
|
||||
ASSERT_EQ(liveUpdates.diag.connectionsOpened, 1);
|
||||
ASSERT_EQ(liveUpdates.diag.connectionsClosed, 1);
|
||||
ASSERT_EQ(liveUpdates.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(liveUpdates.diag().connectionsOpened, 1);
|
||||
ASSERT_EQ(liveUpdates.diag().connectionsClosed, 1);
|
||||
ASSERT_EQ(liveUpdates.diag().connectionsFailed, 0);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QString>
|
||||
#include <QStringBuilder>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||
|
||||
#include "mocks/BaseApplication.hpp"
|
||||
#include "providers/liveupdates/Diag.hpp"
|
||||
#include "providers/seventv/eventapi/Client.hpp"
|
||||
#include "providers/seventv/eventapi/Dispatch.hpp"
|
||||
#include "providers/seventv/eventapi/Message.hpp"
|
||||
#include "Test.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QtCore/qtestsupport_core.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
@@ -19,9 +22,9 @@ const QString TARGET_USER_ID = "60b39e943e203cc169dfc106";
|
||||
|
||||
TEST(SeventvEventAPI, AllEvents)
|
||||
{
|
||||
mock::BaseApplication app;
|
||||
const QString host("wss://127.0.0.1:9050/liveupdates/seventv/all-events");
|
||||
SeventvEventAPI eventAPI(host, std::chrono::milliseconds(1000));
|
||||
eventAPI.start();
|
||||
|
||||
std::optional<EmoteAddDispatch> addDispatch;
|
||||
std::optional<EmoteUpdateDispatch> updateDispatch;
|
||||
@@ -41,13 +44,12 @@ TEST(SeventvEventAPI, AllEvents)
|
||||
userDispatch = d;
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
eventAPI.subscribeUser("", EMOTE_SET_A);
|
||||
std::this_thread::sleep_for(500ms);
|
||||
QTest::qWait(500);
|
||||
|
||||
ASSERT_EQ(eventAPI.diag.connectionsOpened, 1);
|
||||
ASSERT_EQ(eventAPI.diag.connectionsClosed, 0);
|
||||
ASSERT_EQ(eventAPI.diag.connectionsFailed, 0);
|
||||
ASSERT_EQ(eventAPI.diag().connectionsOpened, 1);
|
||||
ASSERT_EQ(eventAPI.diag().connectionsClosed, 0);
|
||||
ASSERT_EQ(eventAPI.diag().connectionsFailed, 0);
|
||||
|
||||
auto add = *addDispatch;
|
||||
ASSERT_EQ(add.emoteSetID, EMOTE_SET_A);
|
||||
@@ -73,7 +75,7 @@ TEST(SeventvEventAPI, AllEvents)
|
||||
removeDispatch = std::nullopt;
|
||||
|
||||
eventAPI.subscribeUser(TARGET_USER_ID, "");
|
||||
std::this_thread::sleep_for(50ms);
|
||||
QTest::qWait(50);
|
||||
|
||||
ASSERT_EQ(addDispatch.has_value(), false);
|
||||
ASSERT_EQ(updateDispatch.has_value(), false);
|
||||
@@ -87,23 +89,26 @@ TEST(SeventvEventAPI, AllEvents)
|
||||
ASSERT_EQ(user.connectionIndex, 0);
|
||||
|
||||
eventAPI.stop();
|
||||
ASSERT_EQ(eventAPI.diag.connectionsOpened, 1);
|
||||
ASSERT_EQ(eventAPI.diag.connectionsClosed, 1);
|
||||
ASSERT_EQ(eventAPI.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(eventAPI.diag().connectionsOpened, 1);
|
||||
ASSERT_EQ(eventAPI.diag().connectionsClosed, 1);
|
||||
ASSERT_EQ(eventAPI.diag().connectionsFailed, 0);
|
||||
}
|
||||
|
||||
TEST(SeventvEventAPI, NoHeartbeat)
|
||||
{
|
||||
mock::BaseApplication app;
|
||||
const QString host("wss://127.0.0.1:9050/liveupdates/seventv/no-heartbeat");
|
||||
SeventvEventAPI eventApi(host, std::chrono::milliseconds(1000));
|
||||
eventApi.start();
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
eventApi.subscribeUser("", EMOTE_SET_A);
|
||||
std::this_thread::sleep_for(1250ms);
|
||||
ASSERT_EQ(eventApi.diag.connectionsOpened, 2);
|
||||
ASSERT_EQ(eventApi.diag.connectionsClosed, 1);
|
||||
ASSERT_EQ(eventApi.diag.connectionsFailed, 0);
|
||||
QTest::qWait(1250);
|
||||
ASSERT_EQ(eventApi.diag().connectionsOpened, 2);
|
||||
ASSERT_EQ(eventApi.diag().connectionsClosed, 1);
|
||||
ASSERT_EQ(eventApi.diag().connectionsFailed, 0);
|
||||
|
||||
eventApi.stop();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user