feat: Add Live Emote Updates for BTTV (#4147)
This feature is enabled by default and can be disabled in settings with the "Enable BTTV live emotes updates" setting. Co-authored-by: Felanbird <41973452+Felanbird@users.noreply.github.com> Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
#include "messages/Image.hpp"
|
||||
#include "messages/ImageSet.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
||||
@@ -21,6 +22,12 @@ namespace {
|
||||
|
||||
QString emoteLinkFormat("https://betterttv.com/emotes/%1");
|
||||
|
||||
struct CreateEmoteResult {
|
||||
EmoteId id;
|
||||
EmoteName name;
|
||||
Emote emote;
|
||||
};
|
||||
|
||||
Url getEmoteLink(QString urlTemplate, const EmoteId &id,
|
||||
const QString &emoteScale)
|
||||
{
|
||||
@@ -70,6 +77,70 @@ namespace {
|
||||
|
||||
return {Success, std::move(emotes)};
|
||||
}
|
||||
|
||||
CreateEmoteResult createChannelEmote(const QString &channelDisplayName,
|
||||
const QJsonObject &jsonEmote)
|
||||
{
|
||||
auto id = EmoteId{jsonEmote.value("id").toString()};
|
||||
auto name = EmoteName{jsonEmote.value("code").toString()};
|
||||
auto author = EmoteAuthor{
|
||||
jsonEmote.value("user").toObject().value("displayName").toString()};
|
||||
|
||||
auto emote = Emote({
|
||||
name,
|
||||
ImageSet{
|
||||
Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25),
|
||||
},
|
||||
Tooltip{
|
||||
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
|
||||
.arg(name.string)
|
||||
// when author is empty, it is a channel emote created by the broadcaster
|
||||
.arg(author.string.isEmpty() ? "Channel" : "Shared")
|
||||
.arg(author.string.isEmpty() ? channelDisplayName
|
||||
: author.string)},
|
||||
Url{emoteLinkFormat.arg(id.string)},
|
||||
false,
|
||||
id,
|
||||
});
|
||||
|
||||
return {id, name, emote};
|
||||
}
|
||||
|
||||
bool updateChannelEmote(Emote &emote, const QString &channelDisplayName,
|
||||
const QJsonObject &jsonEmote)
|
||||
{
|
||||
bool anyModifications = false;
|
||||
|
||||
if (jsonEmote.contains("code"))
|
||||
{
|
||||
emote.name = EmoteName{jsonEmote.value("code").toString()};
|
||||
anyModifications = true;
|
||||
}
|
||||
if (jsonEmote.contains("user"))
|
||||
{
|
||||
emote.author = EmoteAuthor{jsonEmote.value("user")
|
||||
.toObject()
|
||||
.value("displayName")
|
||||
.toString()};
|
||||
anyModifications = true;
|
||||
}
|
||||
|
||||
if (anyModifications)
|
||||
{
|
||||
emote.tooltip = Tooltip{
|
||||
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
|
||||
.arg(emote.name.string)
|
||||
// when author is empty, it is a channel emote created by the broadcaster
|
||||
.arg(emote.author.string.isEmpty() ? "Channel" : "Shared")
|
||||
.arg(emote.author.string.isEmpty() ? channelDisplayName
|
||||
: emote.author.string)};
|
||||
}
|
||||
|
||||
return anyModifications;
|
||||
}
|
||||
|
||||
std::pair<Outcome, EmoteMap> parseChannelEmotes(
|
||||
const QJsonObject &jsonRoot, const QString &channelDisplayName)
|
||||
{
|
||||
@@ -80,33 +151,11 @@ namespace {
|
||||
auto jsonEmotes = jsonRoot.value(key).toArray();
|
||||
for (auto jsonEmote_ : jsonEmotes)
|
||||
{
|
||||
auto jsonEmote = jsonEmote_.toObject();
|
||||
auto emote = createChannelEmote(channelDisplayName,
|
||||
jsonEmote_.toObject());
|
||||
|
||||
auto id = EmoteId{jsonEmote.value("id").toString()};
|
||||
auto name = EmoteName{jsonEmote.value("code").toString()};
|
||||
auto author = EmoteAuthor{jsonEmote.value("user")
|
||||
.toObject()
|
||||
.value("displayName")
|
||||
.toString()};
|
||||
|
||||
auto emote = Emote({
|
||||
name,
|
||||
ImageSet{
|
||||
Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25),
|
||||
},
|
||||
Tooltip{
|
||||
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
|
||||
.arg(name.string)
|
||||
// when author is empty, it is a channel emote created by the broadcaster
|
||||
.arg(author.string.isEmpty() ? "Channel" : "Shared")
|
||||
.arg(author.string.isEmpty() ? channelDisplayName
|
||||
: author.string)},
|
||||
Url{emoteLinkFormat.arg(id.string)},
|
||||
});
|
||||
|
||||
emotes[name] = cachedOrMake(std::move(emote), id);
|
||||
emotes[emote.name] =
|
||||
cachedOrMake(std::move(emote.emote), emote.id);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -227,6 +276,78 @@ void BttvEmotes::loadChannel(std::weak_ptr<Channel> channel,
|
||||
.execute();
|
||||
}
|
||||
|
||||
EmotePtr BttvEmotes::addEmote(
|
||||
const QString &channelDisplayName,
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||
const BttvLiveUpdateEmoteUpdateAddMessage &message)
|
||||
{
|
||||
// This copies the map.
|
||||
EmoteMap updatedMap = *channelEmoteMap.get();
|
||||
auto result = createChannelEmote(channelDisplayName, message.jsonEmote);
|
||||
|
||||
auto emote = std::make_shared<const Emote>(std::move(result.emote));
|
||||
updatedMap[result.name] = emote;
|
||||
channelEmoteMap.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
|
||||
|
||||
return emote;
|
||||
}
|
||||
|
||||
boost::optional<std::pair<EmotePtr, EmotePtr>> BttvEmotes::updateEmote(
|
||||
const QString &channelDisplayName,
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||
const BttvLiveUpdateEmoteUpdateAddMessage &message)
|
||||
{
|
||||
// This copies the map.
|
||||
EmoteMap updatedMap = *channelEmoteMap.get();
|
||||
|
||||
// Step 1: remove the existing emote
|
||||
auto it = updatedMap.findEmote(QString(), message.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 oldEmotePtr = it->second;
|
||||
// copy the existing emote, to not change the original one
|
||||
auto emote = *oldEmotePtr;
|
||||
updatedMap.erase(it);
|
||||
|
||||
// Step 2: update the emote
|
||||
if (!updateChannelEmote(emote, channelDisplayName, message.jsonEmote))
|
||||
{
|
||||
// The emote wasn't actually updated
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
auto name = emote.name;
|
||||
auto emotePtr = std::make_shared<const Emote>(std::move(emote));
|
||||
updatedMap[name] = emotePtr;
|
||||
channelEmoteMap.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
|
||||
|
||||
return std::make_pair(oldEmotePtr, emotePtr);
|
||||
}
|
||||
|
||||
boost::optional<EmotePtr> BttvEmotes::removeEmote(
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||
const BttvLiveUpdateEmoteRemoveMessage &message)
|
||||
{
|
||||
// This copies the map.
|
||||
EmoteMap updatedMap = *channelEmoteMap.get();
|
||||
auto it = updatedMap.findEmote(QString(), message.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);
|
||||
channelEmoteMap.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
|
||||
|
||||
return emote;
|
||||
}
|
||||
|
||||
/*
|
||||
static Url getEmoteLink(QString urlTemplate, const EmoteId &id,
|
||||
const QString &emoteScale)
|
||||
|
||||
@@ -13,6 +13,8 @@ struct Emote;
|
||||
using EmotePtr = std::shared_ptr<const Emote>;
|
||||
class EmoteMap;
|
||||
class Channel;
|
||||
struct BttvLiveUpdateEmoteUpdateAddMessage;
|
||||
struct BttvLiveUpdateEmoteRemoveMessage;
|
||||
|
||||
class BttvEmotes final
|
||||
{
|
||||
@@ -33,6 +35,41 @@ public:
|
||||
std::function<void(EmoteMap &&)> callback,
|
||||
bool manualRefresh);
|
||||
|
||||
/**
|
||||
* Adds an emote to the `channelEmoteMap`.
|
||||
* This will _copy_ the emote map and
|
||||
* update the `Atomic`.
|
||||
*
|
||||
* @return The added emote.
|
||||
*/
|
||||
static EmotePtr addEmote(
|
||||
const QString &channelDisplayName,
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||
const BttvLiveUpdateEmoteUpdateAddMessage &message);
|
||||
|
||||
/**
|
||||
* Updates an emote in this `channelEmoteMap`.
|
||||
* This will _copy_ the emote map and
|
||||
* update the `Atomic`.
|
||||
*
|
||||
* @return pair<old emote, new emote> if any emote was updated.
|
||||
*/
|
||||
static boost::optional<std::pair<EmotePtr, EmotePtr>> updateEmote(
|
||||
const QString &channelDisplayName,
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||
const BttvLiveUpdateEmoteUpdateAddMessage &message);
|
||||
|
||||
/**
|
||||
* Removes an emote from this `channelEmoteMap`.
|
||||
* 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>> &channelEmoteMap,
|
||||
const BttvLiveUpdateEmoteRemoveMessage &message);
|
||||
|
||||
private:
|
||||
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
BttvLiveUpdates::BttvLiveUpdates(QString host)
|
||||
: BasicPubSubManager(std::move(host))
|
||||
{
|
||||
}
|
||||
|
||||
void BttvLiveUpdates::joinChannel(const QString &channelID,
|
||||
const QString &userName)
|
||||
{
|
||||
if (this->joinedChannels_.insert(channelID).second)
|
||||
{
|
||||
this->subscribe({BttvLiveUpdateSubscriptionChannel{channelID}});
|
||||
this->subscribe({BttvLiveUpdateBroadcastMe{.twitchID = channelID,
|
||||
.userName = userName}});
|
||||
}
|
||||
}
|
||||
|
||||
void BttvLiveUpdates::partChannel(const QString &id)
|
||||
{
|
||||
if (this->joinedChannels_.erase(id) > 0)
|
||||
{
|
||||
this->unsubscribe({BttvLiveUpdateSubscriptionChannel{id}});
|
||||
}
|
||||
}
|
||||
|
||||
void BttvLiveUpdates::onMessage(
|
||||
websocketpp::connection_hdl /*hdl*/,
|
||||
BasicPubSubManager<BttvLiveUpdateSubscription>::WebsocketMessagePtr msg)
|
||||
{
|
||||
const auto &payload = QString::fromStdString(msg->get_payload());
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(payload.toUtf8()));
|
||||
|
||||
if (jsonDoc.isNull())
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Failed to parse live update JSON";
|
||||
return;
|
||||
}
|
||||
auto json = jsonDoc.object();
|
||||
|
||||
auto eventType = json["name"].toString();
|
||||
auto eventData = json["data"].toObject();
|
||||
|
||||
if (eventType == "emote_create")
|
||||
{
|
||||
auto message = BttvLiveUpdateEmoteUpdateAddMessage(eventData);
|
||||
|
||||
if (!message.validate())
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Invalid add message" << json;
|
||||
return;
|
||||
}
|
||||
|
||||
this->signals_.emoteAdded.invoke(message);
|
||||
}
|
||||
else if (eventType == "emote_update")
|
||||
{
|
||||
auto message = BttvLiveUpdateEmoteUpdateAddMessage(eventData);
|
||||
|
||||
if (!message.validate())
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Invalid update message" << json;
|
||||
return;
|
||||
}
|
||||
|
||||
this->signals_.emoteUpdated.invoke(message);
|
||||
}
|
||||
else if (eventType == "emote_delete")
|
||||
{
|
||||
auto message = BttvLiveUpdateEmoteRemoveMessage(eventData);
|
||||
|
||||
if (!message.validate())
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Invalid deletion message" << json;
|
||||
return;
|
||||
}
|
||||
|
||||
this->signals_.emoteRemoved.invoke(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoBttv) << "Unhandled event:" << json;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp"
|
||||
#include "providers/liveupdates/BasicPubSubManager.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class BttvLiveUpdates : public BasicPubSubManager<BttvLiveUpdateSubscription>
|
||||
{
|
||||
template <typename T>
|
||||
using Signal =
|
||||
pajlada::Signals::Signal<T>; // type-id is vector<T, Alloc<T>>
|
||||
|
||||
public:
|
||||
BttvLiveUpdates(QString host);
|
||||
|
||||
struct {
|
||||
Signal<BttvLiveUpdateEmoteUpdateAddMessage> emoteAdded;
|
||||
Signal<BttvLiveUpdateEmoteUpdateAddMessage> emoteUpdated;
|
||||
Signal<BttvLiveUpdateEmoteRemoveMessage> emoteRemoved;
|
||||
} signals_; // NOLINT(readability-identifier-naming)
|
||||
|
||||
/**
|
||||
* Joins a Twitch channel by its id (without any prefix like 'twitch:')
|
||||
* if it's not already joined.
|
||||
*
|
||||
* @param channelID the Twitch channel-id of the broadcaster.
|
||||
* @param userName the Twitch username of the current user.
|
||||
*/
|
||||
void joinChannel(const QString &channelID, const QString &userName);
|
||||
|
||||
/**
|
||||
* Parts a twitch channel by its id (without any prefix like 'twitch:')
|
||||
* if it's joined.
|
||||
*
|
||||
* @param id the Twitch channel-id of the broadcaster.
|
||||
*/
|
||||
void partChannel(const QString &id);
|
||||
|
||||
protected:
|
||||
void onMessage(
|
||||
websocketpp::connection_hdl hdl,
|
||||
BasicPubSubManager<BttvLiveUpdateSubscription>::WebsocketMessagePtr msg)
|
||||
override;
|
||||
|
||||
private:
|
||||
// Contains all joined Twitch channel-ids
|
||||
std::unordered_set<QString> joinedChannels_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,52 @@
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
bool tryParseChannelId(QString &channelId)
|
||||
{
|
||||
if (!channelId.startsWith("twitch:"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
channelId.remove(0, 7); // "twitch:"
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
BttvLiveUpdateEmoteUpdateAddMessage::BttvLiveUpdateEmoteUpdateAddMessage(
|
||||
const QJsonObject &json)
|
||||
: channelID(json["channel"].toString())
|
||||
, jsonEmote(json["emote"].toObject())
|
||||
, emoteName(this->jsonEmote["code"].toString())
|
||||
, emoteID(this->jsonEmote["id"].toString())
|
||||
, badChannelID_(!tryParseChannelId(this->channelID))
|
||||
{
|
||||
}
|
||||
|
||||
bool BttvLiveUpdateEmoteUpdateAddMessage::validate() const
|
||||
{
|
||||
// We don't need to check for jsonEmote["code"]/["id"],
|
||||
// because these are this->emoteID and this->emoteName.
|
||||
return !this->badChannelID_ && !this->channelID.isEmpty() &&
|
||||
!this->emoteID.isEmpty() && !this->emoteName.isEmpty();
|
||||
}
|
||||
|
||||
BttvLiveUpdateEmoteRemoveMessage::BttvLiveUpdateEmoteRemoveMessage(
|
||||
const QJsonObject &json)
|
||||
: channelID(json["channel"].toString())
|
||||
, emoteID(json["emoteId"].toString())
|
||||
, badChannelID_(!tryParseChannelId(this->channelID))
|
||||
{
|
||||
}
|
||||
|
||||
bool BttvLiveUpdateEmoteRemoveMessage::validate() const
|
||||
{
|
||||
return !this->badChannelID_ && !this->emoteID.isEmpty() &&
|
||||
!this->channelID.isEmpty();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct BttvLiveUpdateEmoteUpdateAddMessage {
|
||||
BttvLiveUpdateEmoteUpdateAddMessage(const QJsonObject &json);
|
||||
|
||||
QString channelID;
|
||||
|
||||
QJsonObject jsonEmote;
|
||||
QString emoteName;
|
||||
QString emoteID;
|
||||
|
||||
bool validate() const;
|
||||
|
||||
private:
|
||||
// true if the channel id is malformed
|
||||
// (e.g. doesn't start with "twitch:")
|
||||
bool badChannelID_;
|
||||
};
|
||||
|
||||
struct BttvLiveUpdateEmoteRemoveMessage {
|
||||
BttvLiveUpdateEmoteRemoveMessage(const QJsonObject &json);
|
||||
|
||||
QString channelID;
|
||||
QString emoteID;
|
||||
|
||||
bool validate() const;
|
||||
|
||||
private:
|
||||
// true if the channel id is malformed
|
||||
// (e.g. doesn't start with "twitch:")
|
||||
bool badChannelID_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,107 @@
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp"
|
||||
|
||||
#include <QJsonDocument>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
QByteArray BttvLiveUpdateSubscription::encodeSubscribe() const
|
||||
{
|
||||
return QJsonDocument(std::visit(
|
||||
[](const auto &d) {
|
||||
return d.encode(true);
|
||||
},
|
||||
this->data))
|
||||
.toJson();
|
||||
}
|
||||
|
||||
QByteArray BttvLiveUpdateSubscription::encodeUnsubscribe() const
|
||||
{
|
||||
return QJsonDocument(std::visit(
|
||||
[](const auto &d) {
|
||||
return d.encode(false);
|
||||
},
|
||||
this->data))
|
||||
.toJson();
|
||||
}
|
||||
|
||||
QDebug &operator<<(QDebug &dbg, const BttvLiveUpdateSubscription &subscription)
|
||||
{
|
||||
std::visit(
|
||||
[&](const auto &data) {
|
||||
dbg << data;
|
||||
},
|
||||
subscription.data);
|
||||
return dbg;
|
||||
}
|
||||
|
||||
QJsonObject BttvLiveUpdateSubscriptionChannel::encode(bool isSubscribe) const
|
||||
{
|
||||
QJsonObject root;
|
||||
if (isSubscribe)
|
||||
{
|
||||
root["name"] = "join_channel";
|
||||
}
|
||||
else
|
||||
{
|
||||
root["name"] = "part_channel";
|
||||
}
|
||||
|
||||
QJsonObject data;
|
||||
data["name"] = QString("twitch:%1").arg(this->twitchID);
|
||||
|
||||
root["data"] = data;
|
||||
return root;
|
||||
}
|
||||
|
||||
bool BttvLiveUpdateSubscriptionChannel::operator==(
|
||||
const BttvLiveUpdateSubscriptionChannel &rhs) const
|
||||
{
|
||||
return this->twitchID == rhs.twitchID;
|
||||
}
|
||||
|
||||
bool BttvLiveUpdateSubscriptionChannel::operator!=(
|
||||
const BttvLiveUpdateSubscriptionChannel &rhs) const
|
||||
{
|
||||
return !(*this == rhs);
|
||||
}
|
||||
|
||||
QDebug &operator<<(QDebug &dbg, const BttvLiveUpdateSubscriptionChannel &data)
|
||||
{
|
||||
dbg << "BttvLiveUpdateSubscriptionChannel{ twitchID:" << data.twitchID
|
||||
<< '}';
|
||||
return dbg;
|
||||
}
|
||||
|
||||
QJsonObject BttvLiveUpdateBroadcastMe::encode(bool /*isSubscribe*/) const
|
||||
{
|
||||
QJsonObject root;
|
||||
root["name"] = "broadcast_me";
|
||||
|
||||
QJsonObject data;
|
||||
data["name"] = this->userName;
|
||||
data["channel"] = QString("twitch:%1").arg(this->twitchID);
|
||||
|
||||
root["data"] = data;
|
||||
return root;
|
||||
}
|
||||
|
||||
bool BttvLiveUpdateBroadcastMe::operator==(
|
||||
const BttvLiveUpdateBroadcastMe &rhs) const
|
||||
{
|
||||
return this->twitchID == rhs.twitchID && this->userName == rhs.userName;
|
||||
}
|
||||
|
||||
bool BttvLiveUpdateBroadcastMe::operator!=(
|
||||
const BttvLiveUpdateBroadcastMe &rhs) const
|
||||
{
|
||||
return !(*this == rhs);
|
||||
}
|
||||
|
||||
QDebug &operator<<(QDebug &dbg, const BttvLiveUpdateBroadcastMe &data)
|
||||
{
|
||||
dbg << "BttvLiveUpdateBroadcastMe{ twitchID:" << data.twitchID
|
||||
<< "userName:" << data.userName << '}';
|
||||
return dbg;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include <variant>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct BttvLiveUpdateSubscriptionChannel {
|
||||
QString twitchID;
|
||||
|
||||
QJsonObject encode(bool isSubscribe) const;
|
||||
bool operator==(const BttvLiveUpdateSubscriptionChannel &rhs) const;
|
||||
bool operator!=(const BttvLiveUpdateSubscriptionChannel &rhs) const;
|
||||
friend QDebug &operator<<(QDebug &dbg,
|
||||
const BttvLiveUpdateSubscriptionChannel &data);
|
||||
};
|
||||
|
||||
struct BttvLiveUpdateBroadcastMe {
|
||||
QString twitchID;
|
||||
QString userName;
|
||||
|
||||
QJsonObject encode(bool isSubscribe) const;
|
||||
bool operator==(const BttvLiveUpdateBroadcastMe &rhs) const;
|
||||
bool operator!=(const BttvLiveUpdateBroadcastMe &rhs) const;
|
||||
friend QDebug &operator<<(QDebug &dbg,
|
||||
const BttvLiveUpdateBroadcastMe &data);
|
||||
};
|
||||
|
||||
using BttvLiveUpdateSubscriptionData =
|
||||
std::variant<BttvLiveUpdateSubscriptionChannel, BttvLiveUpdateBroadcastMe>;
|
||||
|
||||
struct BttvLiveUpdateSubscription {
|
||||
BttvLiveUpdateSubscriptionData data;
|
||||
|
||||
QByteArray encodeSubscribe() const;
|
||||
QByteArray encodeUnsubscribe() const;
|
||||
|
||||
bool operator==(const BttvLiveUpdateSubscription &rhs) const
|
||||
{
|
||||
return this->data == rhs.data;
|
||||
}
|
||||
bool operator!=(const BttvLiveUpdateSubscription &rhs) const
|
||||
{
|
||||
return !(*this == rhs);
|
||||
}
|
||||
|
||||
friend QDebug &operator<<(QDebug &dbg,
|
||||
const BttvLiveUpdateSubscription &subscription);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
namespace std {
|
||||
|
||||
template <>
|
||||
struct hash<chatterino::BttvLiveUpdateSubscriptionChannel> {
|
||||
size_t operator()(
|
||||
const chatterino::BttvLiveUpdateSubscriptionChannel &data) const
|
||||
{
|
||||
return qHash(data.twitchID);
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct hash<chatterino::BttvLiveUpdateBroadcastMe> {
|
||||
size_t operator()(const chatterino::BttvLiveUpdateBroadcastMe &data) const
|
||||
{
|
||||
size_t seed = 0;
|
||||
boost::hash_combine(seed, qHash(data.twitchID));
|
||||
boost::hash_combine(seed, qHash(data.userName));
|
||||
return seed;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct hash<chatterino::BttvLiveUpdateSubscription> {
|
||||
size_t operator()(const chatterino::BttvLiveUpdateSubscription &sub) const
|
||||
{
|
||||
return std::hash<chatterino::BttvLiveUpdateSubscriptionData>{}(
|
||||
sub.data);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace std
|
||||
Reference in New Issue
Block a user