Refactor 7TV EventAPI (#4342)

* refactor: remove file prefix

* refactor: put eventapi into a namespace

* refactor: support multiple conditions

* fix: handle ack

* docs: add changelog entry

* fix: unsubscribe in destructor

* fix: naming of parameter

* fix: namespaces

* chore: casing

* fix: missing namespace

---------

Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix
2023-02-04 13:42:52 +01:00
committed by GitHub
parent 6516116244
commit 0c74ab17ed
23 changed files with 580 additions and 493 deletions
@@ -1,44 +1,42 @@
#include "providers/seventv/eventapi/SeventvEventAPIClient.hpp"
#include "providers/seventv/eventapi/Client.hpp"
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
#include "providers/seventv/eventapi/Subscription.hpp"
#include "providers/twitch/PubSubHelpers.hpp"
#include <utility>
namespace chatterino {
namespace chatterino::seventv::eventapi {
SeventvEventAPIClient::SeventvEventAPIClient(
liveupdates::WebsocketClient &websocketClient,
liveupdates::WebsocketHandle handle,
std::chrono::milliseconds heartbeatInterval)
: BasicPubSubClient<SeventvEventAPISubscription>(websocketClient,
std::move(handle))
Client::Client(liveupdates::WebsocketClient &websocketClient,
liveupdates::WebsocketHandle handle,
std::chrono::milliseconds heartbeatInterval)
: BasicPubSubClient<Subscription>(websocketClient, std::move(handle))
, lastHeartbeat_(std::chrono::steady_clock::now())
, heartbeatInterval_(heartbeatInterval)
{
}
void SeventvEventAPIClient::onConnectionEstablished()
void Client::onConnectionEstablished()
{
this->lastHeartbeat_.store(std::chrono::steady_clock::now(),
std::memory_order_release);
this->checkHeartbeat();
}
void SeventvEventAPIClient::setHeartbeatInterval(int intervalMs)
void Client::setHeartbeatInterval(int intervalMs)
{
qCDebug(chatterinoSeventvEventAPI)
<< "Setting expected heartbeat interval to" << intervalMs << "ms";
this->heartbeatInterval_ = std::chrono::milliseconds(intervalMs);
}
void SeventvEventAPIClient::handleHeartbeat()
void Client::handleHeartbeat()
{
this->lastHeartbeat_.store(std::chrono::steady_clock::now(),
std::memory_order_release);
}
void SeventvEventAPIClient::checkHeartbeat()
void Client::checkHeartbeat()
{
// Following the heartbeat docs, a connection is dead
// after three missed heartbeats.
@@ -54,8 +52,7 @@ void SeventvEventAPIClient::checkHeartbeat()
return;
}
auto self = std::dynamic_pointer_cast<SeventvEventAPIClient>(
this->shared_from_this());
auto self = std::dynamic_pointer_cast<Client>(this->shared_from_this());
runAfter(this->websocketClient_.get_io_service(), this->heartbeatInterval_,
[self](auto) {
@@ -67,4 +64,4 @@ void SeventvEventAPIClient::checkHeartbeat()
});
}
} // namespace chatterino
} // namespace chatterino::seventv::eventapi
@@ -2,18 +2,22 @@
#include "providers/liveupdates/BasicPubSubClient.hpp"
// this needs to be included for the specialization
// of std::hash for SeventvEventAPISubscription
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
// of std::hash for Subscription
#include "providers/seventv/eventapi/Subscription.hpp"
namespace chatterino {
class SeventvEventAPI;
class SeventvEventAPIClient
: public BasicPubSubClient<SeventvEventAPISubscription>
} // namespace chatterino
namespace chatterino::seventv::eventapi {
class Client : public BasicPubSubClient<Subscription>
{
public:
SeventvEventAPIClient(liveupdates::WebsocketClient &websocketClient,
liveupdates::WebsocketHandle handle,
std::chrono::milliseconds heartbeatInterval);
Client(liveupdates::WebsocketClient &websocketClient,
liveupdates::WebsocketHandle handle,
std::chrono::milliseconds heartbeatInterval);
void setHeartbeatInterval(int intervalMs);
void handleHeartbeat();
@@ -29,7 +33,7 @@ private:
// This will be set once on the welcome message.
std::chrono::milliseconds heartbeatInterval_;
friend class SeventvEventAPI;
friend SeventvEventAPI;
};
} // namespace chatterino
} // namespace chatterino::seventv::eventapi
@@ -1,21 +1,20 @@
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
#include "providers/seventv/eventapi/Dispatch.hpp"
#include <utility>
namespace chatterino {
namespace chatterino::seventv::eventapi {
SeventvEventAPIDispatch::SeventvEventAPIDispatch(QJsonObject obj)
: type(magic_enum::enum_cast<SeventvEventAPISubscriptionType>(
Dispatch::Dispatch(QJsonObject obj)
: type(magic_enum::enum_cast<SubscriptionType>(
obj["type"].toString().toStdString())
.value_or(SeventvEventAPISubscriptionType::INVALID))
.value_or(SubscriptionType::INVALID))
, body(obj["body"].toObject())
, id(this->body["id"].toString())
, actorName(this->body["actor"].toObject()["display_name"].toString())
{
}
SeventvEventAPIEmoteAddDispatch::SeventvEventAPIEmoteAddDispatch(
const SeventvEventAPIDispatch &dispatch, QJsonObject emote)
EmoteAddDispatch::EmoteAddDispatch(const Dispatch &dispatch, QJsonObject emote)
: emoteSetID(dispatch.id)
, actorName(dispatch.actorName)
, emoteJson(std::move(emote))
@@ -23,7 +22,7 @@ SeventvEventAPIEmoteAddDispatch::SeventvEventAPIEmoteAddDispatch(
{
}
bool SeventvEventAPIEmoteAddDispatch::validate() const
bool EmoteAddDispatch::validate() const
{
bool validValues =
!this->emoteSetID.isEmpty() && !this->emoteJson.isEmpty();
@@ -43,8 +42,8 @@ bool SeventvEventAPIEmoteAddDispatch::validate() const
emoteData.contains("owner");
}
SeventvEventAPIEmoteRemoveDispatch::SeventvEventAPIEmoteRemoveDispatch(
const SeventvEventAPIDispatch &dispatch, QJsonObject emote)
EmoteRemoveDispatch::EmoteRemoveDispatch(const Dispatch &dispatch,
QJsonObject emote)
: emoteSetID(dispatch.id)
, actorName(dispatch.actorName)
, emoteName(emote["name"].toString())
@@ -52,15 +51,15 @@ SeventvEventAPIEmoteRemoveDispatch::SeventvEventAPIEmoteRemoveDispatch(
{
}
bool SeventvEventAPIEmoteRemoveDispatch::validate() const
bool EmoteRemoveDispatch::validate() const
{
return !this->emoteSetID.isEmpty() && !this->emoteName.isEmpty() &&
!this->emoteID.isEmpty();
}
SeventvEventAPIEmoteUpdateDispatch::SeventvEventAPIEmoteUpdateDispatch(
const SeventvEventAPIDispatch &dispatch, QJsonObject oldValue,
QJsonObject value)
EmoteUpdateDispatch::EmoteUpdateDispatch(const Dispatch &dispatch,
QJsonObject oldValue,
QJsonObject value)
: emoteSetID(dispatch.id)
, actorName(dispatch.actorName)
, emoteID(value["id"].toString())
@@ -69,17 +68,15 @@ SeventvEventAPIEmoteUpdateDispatch::SeventvEventAPIEmoteUpdateDispatch(
{
}
bool SeventvEventAPIEmoteUpdateDispatch::validate() const
bool EmoteUpdateDispatch::validate() const
{
return !this->emoteSetID.isEmpty() && !this->emoteID.isEmpty() &&
!this->oldEmoteName.isEmpty() && !this->emoteName.isEmpty() &&
this->oldEmoteName != this->emoteName;
}
SeventvEventAPIUserConnectionUpdateDispatch::
SeventvEventAPIUserConnectionUpdateDispatch(
const SeventvEventAPIDispatch &dispatch, const QJsonObject &update,
size_t connectionIndex)
UserConnectionUpdateDispatch::UserConnectionUpdateDispatch(
const Dispatch &dispatch, const QJsonObject &update, size_t connectionIndex)
: userID(dispatch.id)
, actorName(dispatch.actorName)
, oldEmoteSetID(update["old_value"].toObject()["id"].toString())
@@ -88,10 +85,10 @@ SeventvEventAPIUserConnectionUpdateDispatch::
{
}
bool SeventvEventAPIUserConnectionUpdateDispatch::validate() const
bool UserConnectionUpdateDispatch::validate() const
{
return !this->userID.isEmpty() && !this->oldEmoteSetID.isEmpty() &&
!this->emoteSetID.isEmpty();
}
} // namespace chatterino
} // namespace chatterino::seventv::eventapi
@@ -0,0 +1,70 @@
#pragma once
#include "providers/seventv/eventapi/Subscription.hpp"
#include <QJsonObject>
#include <QString>
namespace chatterino::seventv::eventapi {
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#message-payload
struct Dispatch {
SubscriptionType type;
QJsonObject body;
QString id;
// it's okay for this to be empty
QString actorName;
Dispatch(QJsonObject obj);
};
struct EmoteAddDispatch {
QString emoteSetID;
QString actorName;
QJsonObject emoteJson;
QString emoteID;
EmoteAddDispatch(const Dispatch &dispatch, QJsonObject emote);
bool validate() const;
};
struct EmoteRemoveDispatch {
QString emoteSetID;
QString actorName;
QString emoteName;
QString emoteID;
EmoteRemoveDispatch(const Dispatch &dispatch, QJsonObject emote);
bool validate() const;
};
struct EmoteUpdateDispatch {
QString emoteSetID;
QString actorName;
QString emoteID;
QString oldEmoteName;
QString emoteName;
EmoteUpdateDispatch(const Dispatch &dispatch, QJsonObject oldValue,
QJsonObject value);
bool validate() const;
};
struct UserConnectionUpdateDispatch {
QString userID;
QString actorName;
QString oldEmoteSetID;
QString emoteSetID;
size_t connectionIndex;
UserConnectionUpdateDispatch(const Dispatch &dispatch,
const QJsonObject &update,
size_t connectionIndex);
bool validate() const;
};
} // namespace chatterino::seventv::eventapi
@@ -0,0 +1,11 @@
#include "providers/seventv/eventapi/Message.hpp"
namespace chatterino::seventv::eventapi {
Message::Message(QJsonObject _json)
: data(_json["d"].toObject())
, op(Opcode(_json["op"].toInt()))
{
}
} // namespace chatterino::seventv::eventapi
@@ -1,6 +1,6 @@
#pragma once
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
#include "providers/seventv/eventapi/Subscription.hpp"
#include <boost/optional.hpp>
#include <magic_enum.hpp>
@@ -8,27 +8,26 @@
#include <QJsonObject>
#include <QString>
namespace chatterino {
namespace chatterino::seventv::eventapi {
struct SeventvEventAPIMessage {
struct Message {
QJsonObject data;
SeventvEventAPIOpcode op;
Opcode op;
SeventvEventAPIMessage(QJsonObject _json);
Message(QJsonObject _json);
template <class InnerClass>
boost::optional<InnerClass> toInner();
};
template <class InnerClass>
boost::optional<InnerClass> SeventvEventAPIMessage::toInner()
boost::optional<InnerClass> Message::toInner()
{
return InnerClass{this->data};
}
static boost::optional<SeventvEventAPIMessage> parseSeventvEventAPIBaseMessage(
const QString &blob)
static boost::optional<Message> parseBaseMessage(const QString &blob)
{
QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8()));
@@ -37,7 +36,7 @@ static boost::optional<SeventvEventAPIMessage> parseSeventvEventAPIBaseMessage(
return boost::none;
}
return SeventvEventAPIMessage(jsonDoc.object());
return Message(jsonDoc.object());
}
} // namespace chatterino
} // namespace chatterino::seventv::eventapi
@@ -1,72 +0,0 @@
#pragma once
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
#include <QJsonObject>
#include <QString>
namespace chatterino {
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#message-payload
struct SeventvEventAPIDispatch {
SeventvEventAPISubscriptionType type;
QJsonObject body;
QString id;
// it's okay for this to be empty
QString actorName;
SeventvEventAPIDispatch(QJsonObject obj);
};
struct SeventvEventAPIEmoteAddDispatch {
QString emoteSetID;
QString actorName;
QJsonObject emoteJson;
QString emoteID;
SeventvEventAPIEmoteAddDispatch(const SeventvEventAPIDispatch &dispatch,
QJsonObject emote);
bool validate() const;
};
struct SeventvEventAPIEmoteRemoveDispatch {
QString emoteSetID;
QString actorName;
QString emoteName;
QString emoteID;
SeventvEventAPIEmoteRemoveDispatch(const SeventvEventAPIDispatch &dispatch,
QJsonObject emote);
bool validate() const;
};
struct SeventvEventAPIEmoteUpdateDispatch {
QString emoteSetID;
QString actorName;
QString emoteID;
QString oldEmoteName;
QString emoteName;
SeventvEventAPIEmoteUpdateDispatch(const SeventvEventAPIDispatch &dispatch,
QJsonObject oldValue, QJsonObject value);
bool validate() const;
};
struct SeventvEventAPIUserConnectionUpdateDispatch {
QString userID;
QString actorName;
QString oldEmoteSetID;
QString emoteSetID;
size_t connectionIndex;
SeventvEventAPIUserConnectionUpdateDispatch(
const SeventvEventAPIDispatch &dispatch, const QJsonObject &update,
size_t connectionIndex);
bool validate() const;
};
} // namespace chatterino
@@ -1,11 +0,0 @@
#include "providers/seventv/eventapi/SeventvEventAPIMessage.hpp"
namespace chatterino {
SeventvEventAPIMessage::SeventvEventAPIMessage(QJsonObject _json)
: data(_json["d"].toObject())
, op(SeventvEventAPIOpcode(_json["op"].toInt()))
{
}
} // namespace chatterino
@@ -1,80 +0,0 @@
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <tuple>
namespace {
using namespace chatterino;
const char *typeToString(SeventvEventAPISubscriptionType type)
{
switch (type)
{
case SeventvEventAPISubscriptionType::UpdateEmoteSet:
return "emote_set.update";
case SeventvEventAPISubscriptionType::UpdateUser:
return "user.update";
default:
return "";
}
}
QJsonObject createDataJson(const char *typeName, const QString &condition)
{
QJsonObject data;
data["type"] = typeName;
{
QJsonObject conditionObj;
conditionObj["object_id"] = condition;
data["condition"] = conditionObj;
}
return data;
}
} // namespace
namespace chatterino {
bool SeventvEventAPISubscription::operator==(
const SeventvEventAPISubscription &rhs) const
{
return std::tie(this->condition, this->type) ==
std::tie(rhs.condition, rhs.type);
}
bool SeventvEventAPISubscription::operator!=(
const SeventvEventAPISubscription &rhs) const
{
return !(rhs == *this);
}
QByteArray SeventvEventAPISubscription::encodeSubscribe() const
{
const auto *typeName = typeToString(this->type);
QJsonObject root;
root["op"] = (int)SeventvEventAPIOpcode::Subscribe;
root["d"] = createDataJson(typeName, this->condition);
return QJsonDocument(root).toJson();
}
QByteArray SeventvEventAPISubscription::encodeUnsubscribe() const
{
const auto *typeName = typeToString(this->type);
QJsonObject root;
root["op"] = (int)SeventvEventAPIOpcode::Unsubscribe;
root["d"] = createDataJson(typeName, this->condition);
return QJsonDocument(root).toJson();
}
QDebug &operator<<(QDebug &dbg, const SeventvEventAPISubscription &subscription)
{
dbg << "SeventvEventAPISubscription{ condition:" << subscription.condition
<< "type:" << (int)subscription.type << '}';
return dbg;
}
} // namespace chatterino
@@ -1,76 +0,0 @@
#pragma once
#include <magic_enum.hpp>
#include <QByteArray>
#include <QHash>
#include <QString>
namespace chatterino {
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#subscription-types
enum class SeventvEventAPISubscriptionType {
UpdateEmoteSet,
UpdateUser,
INVALID,
};
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#opcodes
enum class SeventvEventAPIOpcode {
Dispatch = 0,
Hello = 1,
Heartbeat = 2,
Reconnect = 4,
Ack = 5,
Error = 6,
EndOfStream = 7,
Identify = 33,
Resume = 34,
Subscribe = 35,
Unsubscribe = 36,
Signal = 37,
};
struct SeventvEventAPISubscription {
bool operator==(const SeventvEventAPISubscription &rhs) const;
bool operator!=(const SeventvEventAPISubscription &rhs) const;
QString condition;
SeventvEventAPISubscriptionType type;
QByteArray encodeSubscribe() const;
QByteArray encodeUnsubscribe() const;
friend QDebug &operator<<(QDebug &dbg,
const SeventvEventAPISubscription &subscription);
};
} // namespace chatterino
template <>
constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name<
chatterino::SeventvEventAPISubscriptionType>(
chatterino::SeventvEventAPISubscriptionType value) noexcept
{
switch (value)
{
case chatterino::SeventvEventAPISubscriptionType::UpdateEmoteSet:
return "emote_set.update";
case chatterino::SeventvEventAPISubscriptionType::UpdateUser:
return "user.update";
default:
return default_tag;
}
}
namespace std {
template <>
struct hash<chatterino::SeventvEventAPISubscription> {
size_t operator()(const chatterino::SeventvEventAPISubscription &sub) const
{
return (size_t)qHash(sub.condition, qHash((int)sub.type));
}
};
} // namespace std
@@ -0,0 +1,105 @@
#include "providers/seventv/eventapi/Subscription.hpp"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <tuple>
#include <utility>
namespace {
using namespace chatterino::seventv::eventapi;
const char *typeToString(SubscriptionType type)
{
return magic_enum::enum_name(type).data();
}
QJsonObject createDataJson(const char *typeName, const Condition &condition)
{
QJsonObject data;
data["type"] = typeName;
data["condition"] = std::visit(
[](const auto &c) {
return c.encode();
},
condition);
return data;
}
} // namespace
namespace chatterino::seventv::eventapi {
bool Subscription::operator==(const Subscription &rhs) const
{
return std::tie(this->condition, this->type) ==
std::tie(rhs.condition, rhs.type);
}
bool Subscription::operator!=(const Subscription &rhs) const
{
return !(rhs == *this);
}
QByteArray Subscription::encodeSubscribe() const
{
const auto *typeName = typeToString(this->type);
QJsonObject root;
root["op"] = (int)Opcode::Subscribe;
root["d"] = createDataJson(typeName, this->condition);
return QJsonDocument(root).toJson();
}
QByteArray Subscription::encodeUnsubscribe() const
{
const auto *typeName = typeToString(this->type);
QJsonObject root;
root["op"] = (int)Opcode::Unsubscribe;
root["d"] = createDataJson(typeName, this->condition);
return QJsonDocument(root).toJson();
}
QDebug &operator<<(QDebug &dbg, const Subscription &subscription)
{
std::visit(
[&](const auto &cond) {
dbg << "Subscription{ condition:" << cond
<< "type:" << magic_enum::enum_name(subscription.type).data()
<< '}';
},
subscription.condition);
return dbg;
}
ObjectIDCondition::ObjectIDCondition(QString objectID)
: objectID(std::move(objectID))
{
}
QJsonObject ObjectIDCondition::encode() const
{
QJsonObject obj;
obj["object_id"] = this->objectID;
return obj;
}
bool ObjectIDCondition::operator==(const ObjectIDCondition &rhs) const
{
return this->objectID == rhs.objectID;
}
bool ObjectIDCondition::operator!=(const ObjectIDCondition &rhs) const
{
return !(*this == rhs);
}
QDebug &operator<<(QDebug &dbg, const ObjectIDCondition &condition)
{
dbg << "{ objectID:" << condition.objectID << "}";
return dbg;
}
} // namespace chatterino::seventv::eventapi
@@ -0,0 +1,106 @@
#pragma once
#include <magic_enum.hpp>
#include <QByteArray>
#include <QHash>
#include <QJsonObject>
#include <QString>
#include <variant>
namespace chatterino::seventv::eventapi {
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#subscription-types
enum class SubscriptionType {
UpdateEmoteSet,
UpdateUser,
INVALID,
};
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#opcodes
enum class Opcode {
Dispatch = 0,
Hello = 1,
Heartbeat = 2,
Reconnect = 4,
Ack = 5,
Error = 6,
EndOfStream = 7,
Identify = 33,
Resume = 34,
Subscribe = 35,
Unsubscribe = 36,
Signal = 37,
};
struct ObjectIDCondition {
ObjectIDCondition(QString objectID);
QString objectID;
QJsonObject encode() const;
friend QDebug &operator<<(QDebug &dbg, const ObjectIDCondition &condition);
bool operator==(const ObjectIDCondition &rhs) const;
bool operator!=(const ObjectIDCondition &rhs) const;
};
using Condition = std::variant<ObjectIDCondition>;
struct Subscription {
bool operator==(const Subscription &rhs) const;
bool operator!=(const Subscription &rhs) const;
Condition condition;
SubscriptionType type;
QByteArray encodeSubscribe() const;
QByteArray encodeUnsubscribe() const;
friend QDebug &operator<<(QDebug &dbg, const Subscription &subscription);
};
} // namespace chatterino::seventv::eventapi
template <>
constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name<
chatterino::seventv::eventapi::SubscriptionType>(
chatterino::seventv::eventapi::SubscriptionType value) noexcept
{
using chatterino::seventv::eventapi::SubscriptionType;
switch (value)
{
case SubscriptionType::UpdateEmoteSet:
return "emote_set.update";
case SubscriptionType::UpdateUser:
return "user.update";
default:
return default_tag;
}
}
namespace std {
template <>
struct hash<chatterino::seventv::eventapi::ObjectIDCondition> {
size_t operator()(
const chatterino::seventv::eventapi::ObjectIDCondition &c) const
{
return (size_t)qHash(c.objectID);
}
};
template <>
struct hash<chatterino::seventv::eventapi::Subscription> {
size_t operator()(
const chatterino::seventv::eventapi::Subscription &sub) const
{
const size_t conditionHash =
std::hash<chatterino::seventv::eventapi::Condition>{}(
sub.condition);
return (size_t)qHash(conditionHash, qHash((int)sub.type));
}
};
} // namespace std