refactor(liveupdates): use WebSocketPool over websocketpp (#6308)

Reviewed-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix
2025-11-08 13:46:10 +01:00
committed by GitHub
parent 6aeb1ca6a7
commit 7f393e2401
26 changed files with 984 additions and 973 deletions
+96 -345
View File
@@ -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
+21 -39
View File
@@ -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
+315 -45
View File
@@ -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
+24 -15
View File
@@ -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
+2 -2
View File
@@ -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())
{
+1 -1
View File
@@ -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