feat: Live Emote Updates for 7TV (#4090)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix
2022-11-13 12:07:41 +01:00
committed by GitHub
parent 8fa89b4073
commit 39f7d8ac4c
35 changed files with 1833 additions and 54 deletions
+153 -13
View File
@@ -32,9 +32,9 @@ using namespace chatterino;
const QString CHANNEL_HAS_NO_EMOTES("This channel has no 7TV channel emotes.");
const QString EMOTE_LINK_FORMAT("https://7tv.app/emotes/%1");
// TODO(nerix): add links to documentation (7tv.io)
const QString API_URL_USER("https://7tv.io/v3/users/twitch/%1");
const QString API_URL_GLOBAL_EMOTE_SET("https://7tv.io/v3/emote-sets/global");
const QString API_URL_EMOTE_SET("https://7tv.io/v3/emote-sets/%1");
struct CreateEmoteResult {
Emote emote;
@@ -160,17 +160,20 @@ CreateEmoteResult createEmote(const QJsonObject &activeEmote,
auto emoteName = EmoteName{activeEmote["name"].toString()};
auto author =
EmoteAuthor{emoteData["owner"].toObject()["display_name"].toString()};
auto baseEmoteName = emoteData["name"].toString();
auto baseEmoteName = EmoteName{emoteData["name"].toString()};
bool zeroWidth = isZeroWidthActive(activeEmote);
bool aliasedName = emoteName.string != baseEmoteName;
bool aliasedName = emoteName != baseEmoteName;
auto tooltip =
aliasedName ? createAliasedTooltip(emoteName.string, baseEmoteName,
author.string, isGlobal)
: createTooltip(emoteName.string, author.string, isGlobal);
aliasedName
? createAliasedTooltip(emoteName.string, baseEmoteName.string,
author.string, isGlobal)
: createTooltip(emoteName.string, author.string, isGlobal);
auto imageSet = makeImageSet(emoteData);
auto emote = Emote({emoteName, imageSet, tooltip,
Url{EMOTE_LINK_FORMAT.arg(emoteId.string)}, zeroWidth});
auto emote =
Emote({emoteName, imageSet, tooltip,
Url{EMOTE_LINK_FORMAT.arg(emoteId.string)}, zeroWidth, emoteId,
author, boost::make_optional(aliasedName, baseEmoteName)});
return {emote, emoteId, emoteName, !emote.images.getImage1()->isEmpty()};
}
@@ -217,6 +220,24 @@ EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, bool isGlobal)
return emotes;
}
EmotePtr createUpdatedEmote(const EmotePtr &oldEmote,
const SeventvEventAPIEmoteUpdateDispatch &dispatch)
{
bool toNonAliased = oldEmote->baseName.has_value() &&
dispatch.emoteName == oldEmote->baseName->string;
auto baseName = oldEmote->baseName.get_value_or(oldEmote->name);
auto emote = std::make_shared<const Emote>(Emote(
{EmoteName{dispatch.emoteName}, oldEmote->images,
toNonAliased
? createTooltip(dispatch.emoteName, oldEmote->author.string, false)
: createAliasedTooltip(dispatch.emoteName, baseName.string,
oldEmote->author.string, false),
oldEmote->homePage, oldEmote->zeroWidth, oldEmote->id,
oldEmote->author, boost::make_optional(!toNonAliased, baseName)}));
return emote;
}
} // namespace
namespace chatterino {
@@ -273,10 +294,9 @@ void SeventvEmotes::loadGlobalEmotes()
.execute();
}
void SeventvEmotes::loadChannelEmotes(const std::weak_ptr<Channel> &channel,
const QString &channelId,
std::function<void(EmoteMap &&)> callback,
bool manualRefresh)
void SeventvEmotes::loadChannelEmotes(
const std::weak_ptr<Channel> &channel, const QString &channelId,
std::function<void(EmoteMap &&, ChannelInfo)> callback, bool manualRefresh)
{
qCDebug(chatterinoSeventv)
<< "Reloading 7TV Channel Emotes" << channelId << manualRefresh;
@@ -298,7 +318,21 @@ void SeventvEmotes::loadChannelEmotes(const std::weak_ptr<Channel> &channel,
if (hasEmotes)
{
callback(std::move(emoteMap));
auto user = json["user"].toObject();
size_t connectionIdx = 0;
for (const auto &conn : user["connections"].toArray())
{
if (conn.toObject()["platform"].toString() == "TWITCH")
{
break;
}
connectionIdx++;
}
callback(std::move(emoteMap),
{user["id"].toString(), emoteSet["id"].toString(),
connectionIdx});
}
auto shared = channel.lock();
@@ -362,4 +396,110 @@ void SeventvEmotes::loadChannelEmotes(const std::weak_ptr<Channel> &channel,
.execute();
}
boost::optional<EmotePtr> SeventvEmotes::addEmote(
Atomic<std::shared_ptr<const EmoteMap>> &map,
const SeventvEventAPIEmoteAddDispatch &dispatch)
{
// Check for visibility first, so we don't copy the map.
auto emoteData = dispatch.emoteJson["data"].toObject();
if (emoteData.empty() || !checkEmoteVisibility(emoteData))
{
return boost::none;
}
// This copies the map.
EmoteMap updatedMap = *map.get();
auto result = createEmote(dispatch.emoteJson, emoteData, false);
if (!result.hasImages)
{
// Incoming emote didn't contain any images, abort
qCDebug(chatterinoSeventv)
<< "Emote without images:" << dispatch.emoteJson;
return boost::none;
}
auto emote = std::make_shared<const Emote>(std::move(result.emote));
updatedMap[result.name] = emote;
map.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
return emote;
}
boost::optional<EmotePtr> SeventvEmotes::updateEmote(
Atomic<std::shared_ptr<const EmoteMap>> &map,
const SeventvEventAPIEmoteUpdateDispatch &dispatch)
{
auto oldMap = map.get();
auto oldEmote = oldMap->findEmote(dispatch.emoteName, dispatch.emoteID);
if (oldEmote == oldMap->end())
{
return boost::none;
}
// This copies the map.
EmoteMap updatedMap = *map.get();
updatedMap.erase(oldEmote->second->name);
auto emote = createUpdatedEmote(oldEmote->second, dispatch);
updatedMap[emote->name] = emote;
map.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
return emote;
}
boost::optional<EmotePtr> SeventvEmotes::removeEmote(
Atomic<std::shared_ptr<const EmoteMap>> &map,
const SeventvEventAPIEmoteRemoveDispatch &dispatch)
{
// This copies the map.
EmoteMap updatedMap = *map.get();
auto it = updatedMap.findEmote(dispatch.emoteName, dispatch.emoteID);
if (it == updatedMap.end())
{
// We already copied the map at this point and are now discarding the copy.
// This is fine, because this case should be really rare.
return boost::none;
}
auto emote = it->second;
updatedMap.erase(it);
map.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
return emote;
}
void SeventvEmotes::getEmoteSet(
const QString &emoteSetId,
std::function<void(EmoteMap &&, QString)> successCallback,
std::function<void(QString)> errorCallback)
{
qCDebug(chatterinoSeventv) << "Loading 7TV Emote Set" << emoteSetId;
NetworkRequest(API_URL_EMOTE_SET.arg(emoteSetId), NetworkRequestType::Get)
.timeout(20000)
.onSuccess([callback = std::move(successCallback),
emoteSetId](const NetworkResult &result) -> Outcome {
auto json = result.parseJson();
auto parsedEmotes = json["emotes"].toArray();
auto emoteMap = parseEmotes(parsedEmotes, false);
qCDebug(chatterinoSeventv) << "Loaded" << emoteMap.size()
<< "7TV Emotes from" << emoteSetId;
callback(std::move(emoteMap), json["name"].toString());
return Success;
})
.onError([emoteSetId, callback = std::move(errorCallback)](
const NetworkResult &result) {
if (result.status() == NetworkResult::timedoutStatus)
{
callback("timed out");
}
else
{
callback(QString("status: %1").arg(result.status()));
}
})
.execute();
}
} // namespace chatterino
+50 -4
View File
@@ -3,6 +3,7 @@
#include "boost/optional.hpp"
#include "common/Aliases.hpp"
#include "common/Atomic.hpp"
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include <memory>
@@ -56,15 +57,60 @@ class EmoteMap;
class SeventvEmotes final
{
public:
struct ChannelInfo {
QString userID;
QString emoteSetID;
size_t twitchConnectionIndex;
};
SeventvEmotes();
std::shared_ptr<const EmoteMap> globalEmotes() const;
boost::optional<EmotePtr> globalEmote(const EmoteName &name) const;
void loadGlobalEmotes();
static void loadChannelEmotes(const std::weak_ptr<Channel> &channel,
const QString &channelId,
std::function<void(EmoteMap &&)> callback,
bool manualRefresh);
static void loadChannelEmotes(
const std::weak_ptr<Channel> &channel, const QString &channelId,
std::function<void(EmoteMap &&, ChannelInfo)> callback,
bool manualRefresh);
/**
* Adds an emote to the `map` if it's valid.
* This will _copy_ the emote map and
* update the `Atomic`.
*
* @return The added emote if an emote was added.
*/
static boost::optional<EmotePtr> addEmote(
Atomic<std::shared_ptr<const EmoteMap>> &map,
const SeventvEventAPIEmoteAddDispatch &dispatch);
/**
* Updates an emote in this `map`.
* This will _copy_ the emote map and
* update the `Atomic`.
*
* @return The updated emote if any emote was updated.
*/
static boost::optional<EmotePtr> updateEmote(
Atomic<std::shared_ptr<const EmoteMap>> &map,
const SeventvEventAPIEmoteUpdateDispatch &dispatch);
/**
* Removes an emote from this `map`.
* This will _copy_ the emote map and
* update the `Atomic`.
*
* @return The removed emote if any emote was removed.
*/
static boost::optional<EmotePtr> removeEmote(
Atomic<std::shared_ptr<const EmoteMap>> &map,
const SeventvEventAPIEmoteRemoveDispatch &dispatch);
/** Fetches an emote-set by its id */
static void getEmoteSet(
const QString &emoteSetId,
std::function<void(EmoteMap &&, QString)> successCallback,
std::function<void(QString)> errorCallback);
private:
Atomic<std::shared_ptr<const EmoteMap>> global_;
+249
View File
@@ -0,0 +1,249 @@
#include "providers/seventv/SeventvEventAPI.hpp"
#include "providers/seventv/eventapi/SeventvEventAPIClient.hpp"
#include "providers/seventv/eventapi/SeventvEventAPIMessage.hpp"
#include <QJsonArray>
#include <utility>
namespace chatterino {
SeventvEventAPI::SeventvEventAPI(
QString host, std::chrono::milliseconds defaultHeartbeatInterval)
: BasicPubSubManager(std::move(host))
, heartbeatInterval_(defaultHeartbeatInterval)
{
}
void SeventvEventAPI::subscribeUser(const QString &userID,
const QString &emoteSetID)
{
if (!userID.isEmpty() && this->subscribedUsers_.insert(userID).second)
{
this->subscribe({userID, SeventvEventAPISubscriptionType::UpdateUser});
}
if (!emoteSetID.isEmpty() &&
this->subscribedEmoteSets_.insert(emoteSetID).second)
{
this->subscribe(
{emoteSetID, SeventvEventAPISubscriptionType::UpdateEmoteSet});
}
}
void SeventvEventAPI::unsubscribeEmoteSet(const QString &id)
{
if (this->subscribedEmoteSets_.erase(id) > 0)
{
this->unsubscribe(
{id, SeventvEventAPISubscriptionType::UpdateEmoteSet});
}
}
void SeventvEventAPI::unsubscribeUser(const QString &id)
{
if (this->subscribedUsers_.erase(id) > 0)
{
this->unsubscribe({id, SeventvEventAPISubscriptionType::UpdateUser});
}
}
std::shared_ptr<BasicPubSubClient<SeventvEventAPISubscription>>
SeventvEventAPI::createClient(liveupdates::WebsocketClient &client,
websocketpp::connection_hdl hdl)
{
auto shared = std::make_shared<SeventvEventAPIClient>(
client, hdl, this->heartbeatInterval_);
return std::static_pointer_cast<
BasicPubSubClient<SeventvEventAPISubscription>>(std::move(shared));
}
void SeventvEventAPI::onMessage(
websocketpp::connection_hdl hdl,
BasicPubSubManager<SeventvEventAPISubscription>::WebsocketMessagePtr msg)
{
const auto &payload = QString::fromStdString(msg->get_payload());
auto pMessage = parseSeventvEventAPIBaseMessage(payload);
if (!pMessage)
{
qCDebug(chatterinoSeventvEventAPI)
<< "Unable to parse incoming event-api message: " << payload;
return;
}
auto message = *pMessage;
switch (message.op)
{
case SeventvEventAPIOpcode::Hello: {
if (auto client = this->findClient(hdl))
{
if (auto *stvClient =
dynamic_cast<SeventvEventAPIClient *>(client.get()))
{
stvClient->setHeartbeatInterval(
message.data["heartbeat_interval"].toInt());
}
}
}
break;
case SeventvEventAPIOpcode::Heartbeat: {
if (auto client = this->findClient(hdl))
{
if (auto *stvClient =
dynamic_cast<SeventvEventAPIClient *>(client.get()))
{
stvClient->handleHeartbeat();
}
}
}
break;
case SeventvEventAPIOpcode::Dispatch: {
auto dispatch = message.toInner<SeventvEventAPIDispatch>();
if (!dispatch)
{
qCDebug(chatterinoSeventvEventAPI)
<< "Malformed dispatch" << payload;
return;
}
this->handleDispatch(*dispatch);
}
break;
case SeventvEventAPIOpcode::Reconnect: {
if (auto client = this->findClient(hdl))
{
if (auto *stvClient =
dynamic_cast<SeventvEventAPIClient *>(client.get()))
{
stvClient->close("Reconnecting");
}
}
}
break;
default: {
qCDebug(chatterinoSeventvEventAPI) << "Unhandled op: " << payload;
}
break;
}
}
void SeventvEventAPI::handleDispatch(const SeventvEventAPIDispatch &dispatch)
{
switch (dispatch.type)
{
case SeventvEventAPISubscriptionType::UpdateEmoteSet: {
// 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;
}
SeventvEventAPIEmoteAddDispatch 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;
}
SeventvEventAPIEmoteUpdateDispatch 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;
}
SeventvEventAPIEmoteRemoveDispatch removed(
dispatch, pulled["old_value"].toObject());
if (removed.validate())
{
this->signals_.emoteRemoved.invoke(removed);
}
else
{
qCDebug(chatterinoSeventvEventAPI)
<< "Invalid dispatch" << dispatch.body;
}
}
}
break;
case SeventvEventAPISubscriptionType::UpdateUser: {
// 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;
}
SeventvEventAPIUserConnectionUpdateDispatch update(
dispatch, value, (size_t)updated["index"].toInt());
if (update.validate())
{
this->signals_.userUpdated.invoke(update);
}
else
{
qCDebug(chatterinoSeventvEventAPI)
<< "Invalid dispatch" << dispatch.body;
}
}
}
}
break;
default: {
qCDebug(chatterinoSeventvEventAPI)
<< "Unknown subscription type:" << (int)dispatch.type
<< "body:" << dispatch.body;
}
break;
}
}
} // namespace chatterino
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include "providers/liveupdates/BasicPubSubClient.hpp"
#include "providers/liveupdates/BasicPubSubManager.hpp"
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
#include "util/QStringHash.hpp"
#include <pajlada/signals/signal.hpp>
namespace chatterino {
class SeventvEventAPI : public BasicPubSubManager<SeventvEventAPISubscription>
{
template <typename T>
using Signal =
pajlada::Signals::Signal<T>; // type-id is vector<T, Alloc<T>>
public:
SeventvEventAPI(QString host,
std::chrono::milliseconds defaultHeartbeatInterval =
std::chrono::milliseconds(25000));
struct {
Signal<SeventvEventAPIEmoteAddDispatch> emoteAdded;
Signal<SeventvEventAPIEmoteUpdateDispatch> emoteUpdated;
Signal<SeventvEventAPIEmoteRemoveDispatch> emoteRemoved;
Signal<SeventvEventAPIUserConnectionUpdateDispatch> userUpdated;
} signals_; // NOLINT(readability-identifier-naming)
/**
* Subscribes to a user and emote-set
* if not already subscribed.
*
* @param userID 7TV user-id, may be empty.
* @param emoteSetID 7TV emote-set-id, may be empty.
*/
void subscribeUser(const QString &userID, const QString &emoteSetID);
/** Unsubscribes from a user by its 7TV user id */
void unsubscribeUser(const QString &id);
/** Unsubscribes from an emote-set by its id */
void unsubscribeEmoteSet(const QString &id);
protected:
std::shared_ptr<BasicPubSubClient<SeventvEventAPISubscription>>
createClient(liveupdates::WebsocketClient &client,
websocketpp::connection_hdl hdl) override;
void onMessage(
websocketpp::connection_hdl hdl,
BasicPubSubManager<SeventvEventAPISubscription>::WebsocketMessagePtr
msg) override;
private:
void handleDispatch(const SeventvEventAPIDispatch &dispatch);
std::unordered_set<QString> subscribedEmoteSets_;
std::unordered_set<QString> subscribedUsers_;
std::chrono::milliseconds heartbeatInterval_;
};
} // namespace chatterino
@@ -0,0 +1,69 @@
#include <utility>
#include "providers/seventv/eventapi/SeventvEventAPIClient.hpp"
#include "providers/twitch/PubSubHelpers.hpp"
namespace chatterino {
SeventvEventAPIClient::SeventvEventAPIClient(
liveupdates::WebsocketClient &websocketClient,
liveupdates::WebsocketHandle handle,
std::chrono::milliseconds heartbeatInterval)
: BasicPubSubClient<SeventvEventAPISubscription>(websocketClient,
std::move(handle))
, lastHeartbeat_(std::chrono::steady_clock::now())
, heartbeatInterval_(heartbeatInterval)
{
}
void SeventvEventAPIClient::onConnectionEstablished()
{
this->lastHeartbeat_.store(std::chrono::steady_clock::now(),
std::memory_order_release);
this->checkHeartbeat();
}
void SeventvEventAPIClient::setHeartbeatInterval(int intervalMs)
{
qCDebug(chatterinoSeventvEventAPI)
<< "Setting expected heartbeat interval to" << intervalMs << "ms";
this->heartbeatInterval_ = std::chrono::milliseconds(intervalMs);
}
void SeventvEventAPIClient::handleHeartbeat()
{
this->lastHeartbeat_.store(std::chrono::steady_clock::now(),
std::memory_order_release);
}
void SeventvEventAPIClient::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_)
{
qCDebug(chatterinoSeventvEventAPI)
<< "Didn't receive a heartbeat in time, disconnecting!";
this->close("Didn't receive a heartbeat in time");
return;
}
auto self = std::dynamic_pointer_cast<SeventvEventAPIClient>(
this->shared_from_this());
runAfter(this->websocketClient_.get_io_service(), this->heartbeatInterval_,
[self](auto) {
if (!self->isStarted())
{
return;
}
self->checkHeartbeat();
});
}
} // namespace chatterino
@@ -0,0 +1,33 @@
#pragma once
#include "providers/liveupdates/BasicPubSubClient.hpp"
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
namespace chatterino {
class SeventvEventAPIClient
: public BasicPubSubClient<SeventvEventAPISubscription>
{
public:
SeventvEventAPIClient(liveupdates::WebsocketClient &websocketClient,
liveupdates::WebsocketHandle handle,
std::chrono::milliseconds heartbeatInterval);
void setHeartbeatInterval(int intervalMs);
void handleHeartbeat();
protected:
void onConnectionEstablished() override;
private:
void checkHeartbeat();
std::atomic<std::chrono::time_point<std::chrono::steady_clock>>
lastHeartbeat_;
// This will be set once on the welcome message.
std::chrono::milliseconds heartbeatInterval_;
friend class SeventvEventAPI;
};
} // namespace chatterino
@@ -0,0 +1,97 @@
#include <utility>
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
namespace chatterino {
SeventvEventAPIDispatch::SeventvEventAPIDispatch(QJsonObject obj)
: type(magic_enum::enum_cast<SeventvEventAPISubscriptionType>(
obj["type"].toString().toStdString())
.value_or(SeventvEventAPISubscriptionType::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)
: emoteSetID(dispatch.id)
, actorName(dispatch.actorName)
, emoteJson(std::move(emote))
, emoteID(this->emoteJson["id"].toString())
{
}
bool SeventvEventAPIEmoteAddDispatch::validate() const
{
bool validValues =
!this->emoteSetID.isEmpty() && !this->emoteJson.isEmpty();
if (!validValues)
{
return false;
}
bool validActiveEmote = this->emoteJson.contains("id") &&
this->emoteJson.contains("name") &&
this->emoteJson.contains("data");
if (!validActiveEmote)
{
return false;
}
auto emoteData = this->emoteJson["data"].toObject();
return emoteData.contains("name") && emoteData.contains("host") &&
emoteData.contains("owner");
}
SeventvEventAPIEmoteRemoveDispatch::SeventvEventAPIEmoteRemoveDispatch(
const SeventvEventAPIDispatch &dispatch, QJsonObject emote)
: emoteSetID(dispatch.id)
, actorName(dispatch.actorName)
, emoteName(emote["name"].toString())
, emoteID(emote["id"].toString())
{
}
bool SeventvEventAPIEmoteRemoveDispatch::validate() const
{
return !this->emoteSetID.isEmpty() && !this->emoteName.isEmpty() &&
!this->emoteID.isEmpty();
}
SeventvEventAPIEmoteUpdateDispatch::SeventvEventAPIEmoteUpdateDispatch(
const SeventvEventAPIDispatch &dispatch, QJsonObject oldValue,
QJsonObject value)
: emoteSetID(dispatch.id)
, actorName(dispatch.actorName)
, emoteID(value["id"].toString())
, oldEmoteName(oldValue["name"].toString())
, emoteName(value["name"].toString())
{
}
bool SeventvEventAPIEmoteUpdateDispatch::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)
: userID(dispatch.id)
, actorName(dispatch.actorName)
, oldEmoteSetID(update["old_value"].toObject()["id"].toString())
, emoteSetID(update["value"].toObject()["id"].toString())
, connectionIndex(connectionIndex)
{
}
bool SeventvEventAPIUserConnectionUpdateDispatch::validate() const
{
return !this->userID.isEmpty() && !this->oldEmoteSetID.isEmpty() &&
!this->emoteSetID.isEmpty();
}
} // namespace chatterino
@@ -0,0 +1,72 @@
#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
@@ -0,0 +1,11 @@
#include "providers/seventv/eventapi/SeventvEventAPIMessage.hpp"
namespace chatterino {
SeventvEventAPIMessage::SeventvEventAPIMessage(QJsonObject _json)
: data(_json["d"].toObject())
, op(SeventvEventAPIOpcode(_json["op"].toInt()))
{
}
} // namespace chatterino
@@ -0,0 +1,44 @@
#pragma once
#include "providers/seventv/SeventvEventAPI.hpp"
#include <boost/optional.hpp>
#include <magic_enum.hpp>
#include <QJsonDocument>
#include <QJsonObject>
#include <QString>
namespace chatterino {
struct SeventvEventAPIMessage {
QJsonObject data;
SeventvEventAPIOpcode op;
SeventvEventAPIMessage(QJsonObject _json);
template <class InnerClass>
boost::optional<InnerClass> toInner();
};
template <class InnerClass>
boost::optional<InnerClass> SeventvEventAPIMessage::toInner()
{
return InnerClass{this->data};
}
static boost::optional<SeventvEventAPIMessage> parseSeventvEventAPIBaseMessage(
const QString &blob)
{
QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8()));
if (jsonDoc.isNull())
{
return boost::none;
}
return SeventvEventAPIMessage(jsonDoc.object());
}
} // namespace chatterino
@@ -0,0 +1,77 @@
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
#include <QJsonDocument>
#include <QJsonObject>
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
@@ -0,0 +1,77 @@
#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