refactor(liveupdates): use WebSocketPool over websocketpp (#6308)
Reviewed-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user